diff --git a/go.mod b/go.mod index af6c35c9..6d45cb27 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module github.com/dtapps/go-library go 1.20 require ( + gitee.com/chunanyong/zorm v1.6.6 github.com/aliyun/aliyun-oss-go-sdk v2.2.6+incompatible github.com/allegro/bigcache/v3 v3.1.0 github.com/baidubce/bce-sdk-go v0.9.143 github.com/basgys/goxml2json v1.1.0 + github.com/bmizerany/pq v0.0.0-20131128184720-da2b95e392c1 github.com/gin-gonic/gin v1.9.0 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 diff --git a/go.sum b/go.sum index a882105a..a5b52a2a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +gitee.com/chunanyong/zorm v1.6.6 h1:XpcHKQblu8nL8QjE2mdtdqnTyOKMhY4aY8lYjZY51wY= +gitee.com/chunanyong/zorm v1.6.6/go.mod h1:Sk+vofBqQXgNrDTe+nWhV6iMXhiBObFHdCo1MfvAdi8= gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -41,6 +43,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/pq v0.0.0-20131128184720-da2b95e392c1 h1:1clOQIolnXGoH1SUo8ZPgdfOWFp/6i8NuRerrVL/TAc= +github.com/bmizerany/pq v0.0.0-20131128184720-da2b95e392c1/go.mod h1:YR6v6TjYGQnPky7rSf5U+AiQ4+EHIVmFYbhHUPo5L2U= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= diff --git a/utils/dorm/zorm.go b/utils/dorm/zorm.go new file mode 100644 index 00000000..076368c0 --- /dev/null +++ b/utils/dorm/zorm.go @@ -0,0 +1,17 @@ +package dorm + +import ( + "gitee.com/chunanyong/zorm" +) + +type ConfigZormClient struct { + Dns string // 地址 +} + +// ZormClient +// https://zorm.cn/ +// https://www.yuque.com/u27016943/nrgi00 +type ZormClient struct { + Db *zorm.DBDao // 驱动 + config *ConfigZormClient // 配置 +} diff --git a/utils/dorm/zorm_mysql.go b/utils/dorm/zorm_mysql.go new file mode 100644 index 00000000..2ea6938d --- /dev/null +++ b/utils/dorm/zorm_mysql.go @@ -0,0 +1,31 @@ +package dorm + +import ( + "errors" + "fmt" + "gitee.com/chunanyong/zorm" + _ "github.com/go-sql-driver/mysql" +) + +func NewZormMysqlClient(config *ConfigZormClient) (*ZormClient, error) { + + var err error + c := &ZormClient{config: config} + + c.Db, err = zorm.NewDBDao(&zorm.DataSourceConfig{ + DSN: c.config.Dns, + DriverName: "mysql", // 数据库驱动名称 + DBType: "mysql", // 数据库类型 + PrintSQL: true, // 是否打印sql + MaxOpenConns: 0, // 数据库最大连接数,默认50 + MaxIdleConns: 0, // 数据库最大空闲连接数,默认50 + ConnMaxLifetimeSecond: 0, // 连接存活秒时间. 默认600(10分钟)后连接被销毁重建. + // 避免数据库主动断开连接,造成死连接.MySQL默认wait_timeout 28800秒(8小时) + DefaultTxOptions: nil, // 事务隔离级别的默认配置,默认为nil + }) + if err != nil { + return nil, errors.New(fmt.Sprintf("连接失败:%v", err)) + } + + return c, nil +} diff --git a/utils/dorm/zorm_postgresql.go b/utils/dorm/zorm_postgresql.go new file mode 100644 index 00000000..c12e6031 --- /dev/null +++ b/utils/dorm/zorm_postgresql.go @@ -0,0 +1,31 @@ +package dorm + +import ( + "errors" + "fmt" + "gitee.com/chunanyong/zorm" + _ "github.com/bmizerany/pq" +) + +func NewZormPostgresqlClient(config *ConfigZormClient) (*ZormClient, error) { + + var err error + c := &ZormClient{config: config} + + c.Db, err = zorm.NewDBDao(&zorm.DataSourceConfig{ + DSN: c.config.Dns, + DriverName: "postgres", // 数据库驱动名称 + DBType: "postgresql", // 数据库类型 + PrintSQL: true, // 是否打印sql + MaxOpenConns: 0, // 数据库最大连接数,默认50 + MaxIdleConns: 0, // 数据库最大空闲连接数,默认50 + ConnMaxLifetimeSecond: 0, // 连接存活秒时间. 默认600(10分钟)后连接被销毁重建. + // 避免数据库主动断开连接,造成死连接.MySQL默认wait_timeout 28800秒(8小时) + DefaultTxOptions: nil, // 事务隔离级别的默认配置,默认为nil + }) + if err != nil { + return nil, errors.New(fmt.Sprintf("连接失败:%v", err)) + } + + return c, nil +} diff --git a/vendor/gitee.com/chunanyong/zorm/.gitignore b/vendor/gitee.com/chunanyong/zorm/.gitignore new file mode 100644 index 00000000..7fce6f1c --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/.gitignore @@ -0,0 +1,34 @@ +# idea ignore +.idea/ +*.ipr +*.iml +*.iws + +.vscode/ + +*.swp + +# temp ignore +*.log +*.cache +*.diff +*.exe +*.exe~ +*.patch +*.tmp +*debug.test +debug.test +go.sum + +# system ignore +.DS_Store +Thumbs.db + +# project +*.cert +*.key +.test +iprepo.txt + + +_output \ No newline at end of file diff --git a/vendor/gitee.com/chunanyong/zorm/CHANGELOG.md b/vendor/gitee.com/chunanyong/zorm/CHANGELOG.md new file mode 100644 index 00000000..9f255218 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/CHANGELOG.md @@ -0,0 +1,286 @@ +v1.6.6 + - 感谢 @encircles 的pr,使用FuncWrapFieldTagName函数自定义Tag列名 + - 简化查询逻辑,统一reBindSQL,不覆盖finder参数值,提升性能 + - 修复获取自增主键异常 + - 完善文档,注释 + +v1.6.5 + - TDengineInsertsColumnName TDengine批量insert语句中是否有列名.默认false没有列名,插入值和数据库列顺序保持一致,减少语句长度 + - 调整FuncGlobalTransaction函数返回值,支持seata-go + - 完善文档,注释 + +v1.6.4 + - 感谢@haifengat 的场景反馈,完善NUMBER类型的数据接收 + - RegisterCustomDriverValueConver函数的 dialectColumnType 参数修改为 Dialect.字段类型 ,例如:dm.TEXT + - 增加FuncDecimalValue函数,设置decimal类型接收值,复写函数自定义decimal实现 + - NewSelectFinder方法参数strs取值第一个字符串 + - 感谢@soldier_of_love 的场景反馈,error日志记录执行的sql和参数值 + - 清理无效的代码和注释 + - 完善文档,注释 + +v1.6.3 + - 感谢@rebens 的场景反馈,增加InsertEntityMapSlice函数,批量保存EntityMap + - 感谢@haifengat 的场景反馈,ICustomDriverValueConver增加structFieldType *reflect.Type入参 + - 感谢@zhou-a-xing 调整匿名结构体字段顺序 + - 感谢@rebens 反馈的问题,避免IEntityMap默认实现IEntityStruct接口 + - 感谢@cucuy 对www.zorm.cn官网的修改 + - 完善文档,注释 + +v1.6.2 + - 捕获panic,赋值给err,避免程序崩溃 + - 增加sqlserver 和 oracle 分页默认order by + - 录制视频教程:https://www.bilibili.com/video/BV1L24y1976U/ + - 完善文档,注释 + +v1.6.1 + - 使用RegisterCustomDriverValueConver函数替代CustomDriverValueMap变量,将 ```zorm.CustomDriverValueMap["*dm.DmClob"] = CustomDMText{}```修改为```zorm.RegisterCustomDriverValueConver("TEXT", CustomDMText{})```,达梦数据库重新复制示例代码,重新复制!!重新复制!!! + - 重写sqlRowsValues函数,支持查询单个字段,Struct类型接收 + - 简化自增序列的实现,使用string代替map[string]string + - 使用OverrideFunc重写zorm的函数,暴露WrapUpdateStructFinder函数 + - 去掉kingbase列的大写转换,修改字符串拼接方式,提升性能 + - BindContextDisableTransaction 用在不使用事务更新数据库的场景,强烈建议不要使用这个方法,更新数据库必须有事务!!! + - 增加查询没有返回列的判断,特殊情况可以使用Query执行更新语句,绕过事务检查(不建议) + - 更新官网 https://zorm.cn + - 完善文档,注释 + +v1.6.0 + - 更新漂亮的logo + - 增加db2数据支持,依赖Limit分页语法 + - DBType即将废弃,更名为Dialect,方便gorm和xorm迁移 + - FuncReadWriteStrategy和GetGTXID函数增加error返回值 + - 修改日志格式,统一加上 -> 符号 + - 曾经偷的懒还是还上吧,类型转换加上err返回值.去掉无用的日期格式转换,驱动获取的并不是[]byte + - 修复Finder.Append和GetSQL为nil的bug + - 完善文档,注释 + +v1.5.9 + - hptx已合并@小口天的pr, [hptx代理模式zorm使用示例](https://github.com/CECTC/hptx-samples/tree/main/http_proxy_zorm) 和 [zorm事务托管hptx示例](https://github.com/CECTC/hptx-samples/tree/main/http_zorm) + - 增加IsInTransaction(ctx)函数,检查是否在事务内 + - 扩展函数统一加上ctx入参,方便场景自定义扩展 + - 取消PrintSQL参数,使用SlowSQLMillis控制输出慢sql语句 + - 完善文档,注释 + +v1.5.8 +更新内容: + - 感谢 @zhou-a-xing 编写TDengine的测试用例,不允许手动拼接 '?' 单引号,强制使用?,书写统一 + - 感谢 @小口天 反馈的bug和编写hptx测试用例,修改全局事务接口方法名,避免和gtx方法名一致造成递归调用 + - 取消自动开启全局事务,必须手动zorm.BindContextEnableGlobalTransaction(ctx)开启全局事务 + - 重构 reBindSQL 函数,在SQL最后执行前统一处理 + - 吐槽很久的switch代替if else + - 完善文档,注释 + +v1.5.7 +更新内容: + - 感谢 @小口天 的辛苦付出,https://gitee.com/wuxiangege/zorm-examples 测试用例已经非常完善. + - 按照反射获取的Struct属性顺序,生成insert语句和update语句 + - 支持TDengine数据库,因TDengine驱动不支持事务,需要设置DisableTransaction=true + - 增加hptx和dbpack分布式事务的支持,细粒度控制是否使用全局事务 + - DisableTransaction用于全局禁用数据库事务,用于不支持事务的数据库驱动. + - 完善文档,注释 + +v1.5.6 +更新内容: + - 感谢@无泪发现Transaction方法返回值为nil的bug,已修复 + - 感谢社区贡献,https://zorm.cn 官网上线,很丑的logo上线 :). + - 支持已经存在的数据库连接 + - 修改panic的异常记录和主键零值判断,用于支持基础类型扩展的主键 + - 完善文档,注释 + +v1.5.5 +更新内容: + - 增加CloseDB函数,关闭数据库连接池 + - 完善文档,注释 + +v1.5.4 +更新内容: + - QueryRow如果查询一个字段,而且这个字段数据库为null,会有异常,没有赋为默认值 + - reflect.Type 类型的参数,修改为 *reflect.Type 指针,包括CustomDriverValueConver接口的参数 + - 完善文档,注释 + +v1.5.3 +更新内容: + - 感谢@Howard.TSE的建议,判断配置是否为空 + - 感谢@haming123反馈性能问题.zorm 1.2.x 版本实现了基础功能,读性能比gorm和xorm快一倍.随着功能持续增加,造成性能下降,目前读性能只快了50%. + - 性能优化,去掉不必要的反射 + - 完善文档,注释 + +v1.5.2 +更新内容: + - 感谢奔跑(@zeqjone)提供的正则,排除不在括号内的from,已经满足绝大部分场景 + - 感谢奔跑(@zeqjone) pr,修复 金仓数据库模型定义中tag数据库列标签与数据库内置关键词冲突时,加双引号处理 + - 升级 decimal 到1.3.1 + - 完善文档,注释 + +v1.5.1 +更新内容: + - 完善文档,注释 + - 注释未使用的代码 + - 先判断error,再执行defer rows.Close() + - 增加微信社区支持(负责人是八块腹肌的单身小伙 @zhou-a-xing) + + +v1.5.0 +更新内容: + - 完善文档,注释 + - 支持clickhouse,更新,删除语句使用SQL92标准语法 + - ID默认使用时间戳+随机数,代替UUID实现 + - 优化SQL提取的正则表达式 + - 集成seata-golang,支持全局托管,不修改业务代码,零侵入分布式事务 + +v1.4.9 +更新内容: + - 完善文档,注释 + - 摊牌了,不装了,就是修改注释,刷刷版本活跃度 + +v1.4.8 +更新内容: + - 完善文档,注释 + - 数据库字段和实体类额外映射时,支持 _ 下划线转驼峰 + +v1.4.7 +更新内容: + - 情人节版本,返回map时,如果无法正常转换值类型,就返回原值,而不是nil + +v1.4.6 +更新内容: + - 完善文档,注释 + - 千行代码,胜他十万,牛气冲天,zorm零依赖.(uuid和decimal这两个工具包竟然有1700行代码) + - 在涉密内网开发环境中,零依赖能减少很多麻烦,做不到请不要说没必要...... + +v1.4.5 +更新内容: + - 增强自定义类型转换的功能 + - 完善文档,注释 + - 非常感谢 @anxuanzi 完善代码生成器 + - 非常感谢 @chien_tung 增加changelog,以后版本发布都会记录changelog + +v1.4.4 +更新内容: + - 如果查询的字段在column tag中没有找到,就会根据名称(不区分大小写)映射到struct的属性上 + - 给QueryRow方法增加 has 的返回值,标识数据库是有一条记录的,各位已经使用的大佬,升级时注意修改代码,非常抱歉*3! + +v1.4.3 +更新内容: + - 正式支持南大通用(gbase)数据库,完成国产四库的适配 + - 增加设置全局事务隔离级别和单个事务的隔离级别 + - 修复触发器自增主键的逻辑bug + - 文档完善和细节调整 + +v1.4.2 +更新内容: + - 正式支持神州通用(shentong)数据库 + - 完善pgsql和kingbase的自增主键返回值支持 + - 七家公司的同学建议查询和golang sql方法命名保持统一.做了一个艰难的决定,修改zorm的部分方法名.全局依次替换字符串即可. +zorm.Query( 替换为 zorm.QueryRow( +zorm.QuerySlice( 替换为 zorm.Query( +zorm.QueryMap( 替换为 zorm.QueryRowMap( +zorm.QueryMapSlice( 替换为 zorm.QueryMap( + +v1.4.1 +更新内容: + - 支持自定义扩展字段映射逻辑 + +v1.4.0 +更新内容: + - 修改多条数据的判断逻辑 + +v1.3.9 +更新内容: + - 支持自定义数据类型,包括json/jsonb + - 非常感谢 @chien_tung 同学反馈的问题, QuerySlice方法支持*[]*struct类型,简化从xorm迁移 + - 其他代码细节优化. + +v1.3.7 +更新内容: + - 非常感谢 @zhou- a- xing 同学(八块腹肌的单身少年)的英文翻译,zorm的核心代码注释已经是中英双语了. + - 非常感谢 @chien_tung 同学反馈的问题,修复主键自增int和init64类型的兼容性. + - 其他代码细节优化. + +v1.3.6 +更新内容: + - 完善注释文档 + - 修复Delete方法的参数类型错误 + - 其他代码细节优化. + +v1.3.5 +更新内容: + - 完善注释文档 + - 兼容处理数据库为null时,基本类型取默认值,感谢@fastabler的pr + - 修复批量保存方法的一个bug:如果slice的长度为1,在pgsql和oracle会出现异常 + - 其他代码细节优化. + +v1.3.4 +更新内容: + - 完善注释文档 + - 取消分页语句必须有order by的限制 + - 支持人大金仓数据库 + - 人大金仓驱动说明: https://help.kingbase.com.cn/doc- view- 8108.html + - 人大金仓kingbase 8核心是基于postgresql 9.6,可以使用 https://github.com/lib/pq 进行测试,生产环境建议使用官方驱动 + +v1.3.3 +更新内容: + - 完善注释文档 + - 增加批量保存Struct对象方法 + - 正式支持达梦数据库 + - 基于达梦官方驱动,发布go mod项目 https://gitee.com/chunanyong/dm + +v1.3.2 +更新内容: + - 增加达梦数据的分页适配 + - 完善调整代码注释 + - 增加存储过程和函数的调用示例 + +v1.3.1 +更新内容: + - 修改方法名称,和gorm和xorm保持相似,降低迁移和学习成本 + - 更新测试用例文档 + +v1.3.0 +更新内容: + - 去掉zap日志依赖,通过复写 FuncLogError FuncLogPanic FuncPrintSQL 实现自定义日志 + - golang版本依赖调整为v1.13 + - 迁移测试用到readygo,zorm项目不依赖任何数据库驱动包 + +v1.2.9 +更新内容: + - IEntityMap支持主键自增或主键序列 + - 更新方法返回影响的行数affected + - 修复 查询IEntityMap时数据库无记录出现异常的bug + - 测试用例即文档 https://gitee.com/chunanyong/readygo/blob/master/test/testzorm/BaseDao_test.go + +v1.2.8 +更新内容: + - 暴露FuncGenerateStringID函数,方便自定义扩展字符串主键ID + - Finder.Append 默认加一个空格,避免手误出现语法错误 + - 缓存字段信息时,使用map代替sync.Map,提高性能 + - 第三方性能压测结果 + +v1.2.6 +更新内容: + - DataSourceConfig 配置区分 DriverName 和 DBType,兼容一种数据库的多个驱动包 + - 不再显示依赖数据库驱动,由使用者确定依赖的数据库驱动包 + +v1.2.5 +更新内容: + - 分页语句必须有明确的order by,避免数据库迁移时出现分页语法不兼容. + - 修复列表查询时,page对象为nil的bug + +v1.2.3 +更新内容: + - 完善数据库支持,目前支持MySQL,SQLServer,Oracle,PostgreSQL,SQLite3 + - 简化数据库读写分离实现,暴露zorm.FuncReadWriteBaseDao函数属性,用于自定义读写分离策略 + - 精简zorm.DataSourceConfig属性,增加PrintSQL属性 + +v1.2.2 +更新内容: + - 修改NewPage()返回Page对象指针,传递时少写一个 & 符号 + - 取消GetDBConnection()方法,使用BindContextConnection()方法进行多个数据库库绑定 + - 隐藏DBConnection对象,不再对外暴露数据库对象,避免手动初始化造成的异常 + +v1.1.8 +更新内容: + - 修复UUID支持 + - 数据库连接和事务隐藏到context.Context为统一参数,符合golang规范,更好的性能 + - 封装logger实现,方便更换log包 + - 增加zorm.UpdateStructNotZeroValue 方法,只更新不为零值的字段 + - 完善测试用例 diff --git a/vendor/gitee.com/chunanyong/zorm/DBDao.go b/vendor/gitee.com/chunanyong/zorm/DBDao.go new file mode 100644 index 00000000..2165bcc5 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/DBDao.go @@ -0,0 +1,1846 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Package zorm 使用原生的sql语句,没有对sql语法做限制.语句使用Finder作为载体 +// 占位符统一使用?,zorm会根据数据库类型,语句执行前会自动替换占位符,postgresql 把?替换成$1,$2...;mssql替换成@P1,@p2...;orace替换成:1,:2... +// zorm使用 ctx context.Context 参数实现事务传播,ctx从web层传递进来即可,例如gin的c.Request.Context() +// zorm的事务操作需要显示使用zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {})开启 +// "package zorm" Use native SQL statements, no restrictions on SQL syntax. Statements use Finder as a carrier +// Use placeholders uniformly "?" "zorm" automatically replaces placeholders before statements are executed,depending on the database type. Replaced with $1, $2... ; Replace MSSQL with @p1,@p2... ; Orace is replaced by :1,:2..., +// "zorm" uses the "ctx context.Context" parameter to achieve transaction propagation,and ctx can be passed in from the web layer, such as "gin's c.Request.Context()", +// "zorm" Transaction operations need to be displayed using "zorm.transaction" (ctx, func(ctx context.context) (interface{}, error) {}) +package zorm + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +// FuncReadWriteStrategy 数据库的读写分离的策略,用于外部重写实现自定义的逻辑,也可以使用ctx标识,处理多库的场景,rwType=0 read,rwType=1 write +// 不能归属到DBDao里,BindContextDBConnection已经是指定数据库的连接了,和这个函数会冲突.就作为读写分离的处理方式 +// 即便是放到DBDao里,因为是多库,BindContextDBConnection函数调用少不了,业务包装一个方法,指定一下读写获取一个DBDao效果是一样的,唯一就是需要根据业务指定一下读写,其实更灵活了 +// FuncReadWriteStrategy Single database read and write separation strategy,used for external replication to implement custom logic, rwType=0 read, rwType=1 write. +// "BindContextDBConnection" is already a connection to the specified database and will conflict with this function. As a single database read and write separation of processing +var FuncReadWriteStrategy = func(ctx context.Context, rwType int) (*DBDao, error) { + if defaultDao == nil { + return nil, errors.New("->FuncReadWriteStrategy-->defaultDao为nil,请检查数据库初始化配置是否正确,主要是DSN,DriverName和Dialect") + } + return defaultDao, nil +} + +// wrapContextStringKey 包装context的key,不直接使用string类型,避免外部直接注入使用 +type wrapContextStringKey string + +// contextDBConnectionValueKey context WithValue的key,不能是基础类型,例如字符串,包装一下 +// The key of context WithValue cannot be a basic type, such as a string, wrap it +const contextDBConnectionValueKey = wrapContextStringKey("contextDBConnectionValueKey") + +// contextTxOptionsKey 事务选项设置TxOptions的key,设置事务的隔离级别 +const contextTxOptionsKey = wrapContextStringKey("contextTxOptionsKey") + +// stringBuilderGrowLen 默认长度 +const stringBuilderGrowLen = 100 + +// DataSourceConfig 数据库连接池的配置 +// DateSourceConfig Database connection pool configuration +type DataSourceConfig struct { + // DSN dataSourceName 连接字符串 + // DSN DataSourceName Database connection string + DSN string + // DriverName 数据库驱动名称:mysql,postgres,oracle(go-ora),sqlserver,sqlite3,go_ibm_db,clickhouse,dm,kingbase,aci,taosSql|taosRestful 和Dialect对应 + // DriverName:mysql,dm,postgres,opi8,sqlserver,sqlite3,go_ibm_db,clickhouse,kingbase,aci,taosSql|taosRestful corresponds to Dialect + DriverName string + // Dialect 数据库方言:mysql,postgresql,oracle,mssql,sqlite,db2,clickhouse,dm,kingbase,shentong,tdengine 和 DriverName 对应 + // Dialect:mysql,postgresql,oracle,mssql,sqlite,db2,clickhouse,dm,kingbase,shentong,tdengine corresponds to DriverName + Dialect string + // Deprecated + // DBType 即将废弃,请使用Dialect属性 + // DBType is about to be deprecated, please use the Dialect property + DBType string + // SlowSQLMillis 慢sql的时间阈值,单位毫秒.小于0是禁用SQL语句输出;等于0是只输出SQL语句,不计算执行时间;大于0是计算SQL执行时间,并且>=SlowSQLMillis值 + SlowSQLMillis int + // MaxOpenConns 数据库最大连接数,默认50 + // MaxOpenConns Maximum number of database connections, Default 50 + MaxOpenConns int + // MaxIdleConns 数据库最大空闲连接数,默认50 + // MaxIdleConns The maximum number of free connections to the database default 50 + MaxIdleConns int + // ConnMaxLifetimeSecond 连接存活秒时间. 默认600(10分钟)后连接被销毁重建.避免数据库主动断开连接,造成死连接.MySQL默认wait_timeout 28800秒(8小时) + // ConnMaxLifetimeSecond (Connection survival time in seconds)Destroy and rebuild the connection after the default 600 seconds (10 minutes) + // Prevent the database from actively disconnecting and causing dead connections. MySQL Default wait_timeout 28800 seconds + ConnMaxLifetimeSecond int + + // DefaultTxOptions 事务隔离级别的默认配置,默认为nil + DefaultTxOptions *sql.TxOptions + + // DisableTransaction 禁用事务,默认false,如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务.为了处理某些数据库不支持事务,比如TDengine + // 禁用事务应该有驱动伪造事务API,不应该由orm实现 + DisableTransaction bool + + // MockSQLDB 用于mock测试的入口,如果MockSQLDB不为nil,则不使用DSN,直接使用MockSQLDB + // db, mock, err := sqlmock.New() + // MockSQLDB *sql.DB + + // FuncGlobalTransaction seata/hptx全局分布式事务的适配函数,返回IGlobalTransaction接口的实现 + // 业务必须调用zorm.BindContextEnableGlobalTransaction(ctx)开启全局分布事务 + // seata-go 的ctx是统一的绑定的是struct,也不是XID字符串. hptx是分离的,所以返回了两个ctx,兼容两个库 + FuncGlobalTransaction func(ctx context.Context) (IGlobalTransaction, context.Context, context.Context, error) + + // DisableAutoGlobalTransaction 属性已废弃,请勿使用,相关注释仅作记录备忘 + // DisableAutoGlobalTransaction 禁用自动全局分布式事务,默认false,虽然设置了FuncGlobalTransaction,但是并不想全部业务自动开启全局事务 + // DisableAutoGlobalTransaction = false; ctx,_=zorm.BindContextEnableGlobalTransaction(ctx,false) 默认使用全局事务,ctx绑定为false才不开启 + // DisableAutoGlobalTransaction = true; ctx,_=zorm.BindContextEnableGlobalTransaction(ctx,true) 默认禁用全局事务,ctx绑定为true才开启 + // DisableAutoGlobalTransaction bool + + // SQLDB 使用现有的数据库连接,优先级高于DSN + SQLDB *sql.DB + + // TDengineInsertsColumnName TDengine批量insert语句中是否有列名.默认false没有列名,插入值和数据库列顺序保持一致,减少语句长度 + TDengineInsertsColumnName bool +} + +// DBDao 数据库操作基类,隔离原生操作数据库API入口,所有数据库操作必须通过DBDao进行 +// DBDao Database operation base class, isolate the native operation database API entry,all database operations must be performed through DB Dao +type DBDao struct { + config *DataSourceConfig + dataSource *dataSource +} + +var defaultDao *DBDao = nil + +// NewDBDao 创建dbDao,一个数据库要只执行一次,业务自行控制 +// 第一个执行的数据库为 defaultDao,后续zorm.xxx方法,默认使用的就是defaultDao +// NewDBDao Creates dbDao, a database must be executed only once, and the business is controlled by itself +// The first database to be executed is defaultDao, and the subsequent zorm.xxx method is defaultDao by default +func NewDBDao(config *DataSourceConfig) (*DBDao, error) { + dataSource, err := newDataSource(config) + if err != nil { + err = fmt.Errorf("->NewDBDao创建dataSource失败:%w", err) + FuncLogError(nil, err) + return nil, err + } + dbdao, err := FuncReadWriteStrategy(nil, 1) + if dbdao == nil { + defaultDao = &DBDao{config, dataSource} + return defaultDao, nil + } + if err != nil { + return dbdao, err + } + return &DBDao{config, dataSource}, nil +} + +// newDBConnection 获取一个dbConnection +// 如果参数dbConnection为nil,使用默认的datasource进行获取dbConnection +// 如果是多库,Dao手动调用newDBConnection(),获得dbConnection,WithValue绑定到子context +// newDBConnection Get a db Connection +// If the parameter db Connection is nil, use the default datasource to get db Connection. +// If it is multi-database, Dao manually calls new DB Connection() to obtain db Connection, and With Value is bound to the sub-context +func (dbDao *DBDao) newDBConnection() (*dataBaseConnection, error) { + if dbDao == nil || dbDao.dataSource == nil { + return nil, errors.New("->newDBConnection-->请不要自己创建dbDao,请使用NewDBDao方法进行创建") + } + dbConnection := new(dataBaseConnection) + dbConnection.db = dbDao.dataSource.DB + dbConnection.config = dbDao.config + return dbConnection, nil +} + +// BindContextDBConnection 多库的时候,通过dbDao创建DBConnection绑定到子context,返回的context就有了DBConnection. parent 不能为空 +// BindContextDBConnection In the case of multiple databases, create a DB Connection through db Dao and bind it to a sub-context,and the returned context will have a DB Connection. parent is not nil +func (dbDao *DBDao) BindContextDBConnection(parent context.Context) (context.Context, error) { + if parent == nil { + return nil, errors.New("->BindContextDBConnection-->context的parent不能为nil") + } + dbConnection, errDBConnection := dbDao.newDBConnection() + if errDBConnection != nil { + return parent, errDBConnection + } + ctx := context.WithValue(parent, contextDBConnectionValueKey, dbConnection) + return ctx, nil +} + +// BindContextTxOptions 绑定事务的隔离级别,参考sql.IsolationLevel,如果txOptions为nil,使用默认的事务隔离级别.parent不能为空 +// 需要在事务开启前调用,也就是zorm.Transaction方法前,不然事务开启之后再调用就无效了 +func (dbDao *DBDao) BindContextTxOptions(parent context.Context, txOptions *sql.TxOptions) (context.Context, error) { + if parent == nil { + return nil, errors.New("->BindContextTxOptions-->context的parent不能为nil") + } + + ctx := context.WithValue(parent, contextTxOptionsKey, txOptions) + return ctx, nil +} + +// CloseDB 关闭所有数据库连接 +// 请谨慎调用这个方法,会关闭所有数据库连接,用于处理特殊场景,正常使用无需手动关闭数据库连接 +func (dbDao *DBDao) CloseDB() error { + if dbDao == nil || dbDao.dataSource == nil { + return errors.New("->CloseDB-->请不要自己创建dbDao,请使用NewDBDao方法进行创建") + } + return dbDao.dataSource.Close() +} + +/* +Transaction 的示例代码 + //匿名函数return的error如果不为nil,事务就会回滚 + zorm.Transaction(ctx context.Context,func(ctx context.Context) (interface{}, error) { + + //业务代码 + + + //return的error如果不为nil,事务就会回滚 + return nil, nil + }) +*/ +// 事务方法,隔离dbConnection相关的API.必须通过这个方法进行事务处理,统一事务方式.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 +// 如果入参ctx中没有dbConnection,使用defaultDao开启事务并最后提交 +// 如果入参ctx有dbConnection且没有事务,调用dbConnection.begin()开启事务并最后提交 +// 如果入参ctx有dbConnection且有事务,只使用不提交,有开启方提交事务 +// 但是如果遇到错误或者异常,虽然不是事务的开启方,也会回滚事务,让事务尽早回滚 +// 在多库的场景,手动获取dbConnection,然后绑定到一个新的context,传入进来 +// 不要去掉匿名函数的context参数,因为如果Transaction的context中没有dbConnection,会新建一个context并放入dbConnection,此时的context指针已经变化,不能直接使用Transaction的context参数 +// bug(springrain)如果有大神修改了匿名函数内的参数名,例如改为ctx2,这样业务代码实际使用的是Transaction的context参数,如果为没有dbConnection,会抛异常,如果有dbConnection,实际就是一个对象.影响有限.也可以把匿名函数抽到外部 +// 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别,例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions +// return的error如果不为nil,事务就会回滚 +// 如果使用了分布式事务,需要设置分布式事务函数zorm.DataSourceConfig.FuncGlobalTransaction,实现IGlobalTransaction接口 +// 如果是分布式事务开启方,需要在本地事务前开启分布事务,开启之后获取XID,设值到ctx的XID和TX_XID.XID是seata/hptx MySQL驱动需要,TX_XID是gtxContext.NewRootContext需要 +// 分布式事务需要传递XID,接收方context.WithValue(ctx, "XID", XID)绑定到ctx +// 如果分支事务出现异常或者回滚,会立即回滚分布式事务 +// Transaction method, isolate db Connection related API. This method must be used for transaction processing and unified transaction mode +// If there is no db Connection in the input ctx, use default Dao to start the transaction and submit it finally +// If the input ctx has db Connection and no transaction, call db Connection.begin() to start the transaction and finally commit +// If the input ctx has a db Connection and a transaction, only use non-commit, and the open party submits the transaction +// If you encounter an error or exception, although it is not the initiator of the transaction, the transaction will be rolled back, +// so that the transaction can be rolled back as soon as possible +// In a multi-database scenario, manually obtain db Connection, then bind it to a new context and pass in +// Do not drop the anonymous function's context parameter, because if the Transaction context does not have a DBConnection, +// then a new context will be created and placed in the DBConnection +// The context pointer has changed and the Transaction context parameters cannot be used directly +// "bug (springrain)" If a great god changes the parameter name in the anonymous function, for example, change it to ctx 2, +// so that the business code actually uses the context parameter of Transaction. If there is no db Connection, +// an exception will be thrown. If there is a db Connection, the actual It is an object +// The impact is limited. Anonymous functions can also be extracted outside +// If the return error is not nil, the transaction will be rolled back +func Transaction(ctx context.Context, doTransaction func(ctx context.Context) (interface{}, error)) (interface{}, error) { + return transaction(ctx, doTransaction) +} + +var transaction = func(ctx context.Context, doTransaction func(ctx context.Context) (interface{}, error)) (info interface{}, err error) { + // 是否是dbConnection的开启方,如果是开启方,才可以提交事务 + // Whether it is the opener of db Connection, if it is the opener, the transaction can be submitted + localTxOpen := false + // 是否是分布式事务的开启方.如果ctx中没有xid,认为是开启方 + globalTxOpen := false + // 如果dbConnection不存在,则会用默认的datasource开启事务 + // If db Connection does not exist, the default datasource will be used to start the transaction + var dbConnection *dataBaseConnection + ctx, dbConnection, err = checkDBConnection(ctx, dbConnection, false, 1) + if err != nil { + FuncLogError(ctx, err) + return nil, err + } + + // 适配全局事务的函数 + funcGlobalTx := dbConnection.config.FuncGlobalTransaction + + // 实现IGlobalTransaction接口的事务对象 + var globalTransaction IGlobalTransaction + // 分布式事务的 rootContext,和业务的ctx区别开来,如果业务ctx使用WithValue,就会出现差异 + var globalRootContext context.Context + // 分布式事务的异常 + var errGlobal error + + // 如果没有事务,并且事务没有被禁用,开启事务 + // 开启本地事务前,需要拿到分布式事务对象 + if dbConnection.tx == nil && (!getContextBoolValue(ctx, contextDisableTransactionValueKey, dbConnection.config.DisableTransaction)) { + // if dbConnection.tx == nil { + // 是否使用分布式事务 + enableGlobalTransaction := funcGlobalTx != nil + if enableGlobalTransaction { // 判断ctx里是否有绑定 enableGlobalTransaction + /* + ctxGTXval := ctx.Value(contextEnableGlobalTransactionValueKey) + if ctxGTXval != nil { //如果有值 + enableGlobalTransaction = ctxGTXval.(bool) + } else { //如果ctx没有值,就取值DisableAutoGlobalTransaction + //enableGlobalTransaction = !dbConnection.config.DisableAutoGlobalTransaction + enableGlobalTransaction = false + } + */ + enableGlobalTransaction = getContextBoolValue(ctx, contextEnableGlobalTransactionValueKey, false) + } + + // 需要开启分布式事务,初始化分布式事务对象,判断是否是分布式事务入口 + if enableGlobalTransaction { + // 获取分布式事务的XID + ctxXIDval := ctx.Value("XID") + if ctxXIDval != nil { // 如果本地ctx中有XID + globalXID, _ := ctxXIDval.(string) + // 不知道为什么需要两个Key,还需要请教seata/hptx团队 + // seata/hptx mysql驱动需要 XID,gtxContext.NewRootContext 需要 TX_XID + ctx = context.WithValue(ctx, "TX_XID", globalXID) + } else { // 如果本地ctx中没有XID,也就是没有传递过来XID,认为是分布式事务的开启方.ctx中没有XID和TX_XID的值 + globalTxOpen = true + } + // 获取分布式事务实现对象,用于控制事务提交和回滚.分支事务需要ctx中TX_XID有值,将分支事务关联到主事务 + globalTransaction, ctx, globalRootContext, errGlobal = funcGlobalTx(ctx) + if errGlobal != nil { + errGlobal = fmt.Errorf("->Transaction-->global:Transaction FuncGlobalTransaction获取IGlobalTransaction接口实现失败:%w ", errGlobal) + FuncLogError(ctx, errGlobal) + return nil, errGlobal + } + if globalTransaction == nil || globalRootContext == nil { + errGlobal = errors.New("->Transaction-->global:Transaction FuncGlobalTransaction获取IGlobalTransaction接口的实现为nil ") + FuncLogError(ctx, errGlobal) + return nil, errGlobal + } + + } + if globalTxOpen { // 如果是分布事务开启方,启动分布式事务 + errGlobal = globalTransaction.BeginGTX(ctx, globalRootContext) + if errGlobal != nil { + errGlobal = fmt.Errorf("->Transaction-->global:Transaction 分布式事务开启失败:%w ", errGlobal) + FuncLogError(ctx, errGlobal) + return nil, errGlobal + } + + // 分布式事务开启成功,获取XID,设置到ctx的XID和TX_XID + // seata/hptx mysql驱动需要 XID,gtxContext.NewRootContext 需要 TX_XID + globalXID, errGlobal := globalTransaction.GetGTXID(ctx, globalRootContext) + if errGlobal != nil { + FuncLogError(ctx, errGlobal) + return nil, errGlobal + } + if globalXID == "" { + errGlobal = errors.New("->Transaction-->global:globalTransaction.Begin无异常开启后,获取的XID为空") + FuncLogError(ctx, errGlobal) + return nil, errGlobal + } + ctx = context.WithValue(ctx, "XID", globalXID) + ctx = context.WithValue(ctx, "TX_XID", globalXID) + } + + // 开启本地事务/分支事务 + errBeginTx := dbConnection.beginTx(ctx) + if errBeginTx != nil { + errBeginTx = fmt.Errorf("->Transaction 事务开启失败:%w ", errBeginTx) + FuncLogError(ctx, errBeginTx) + return nil, errBeginTx + } + // 本方法开启的事务,由本方法提交 + // The transaction opened by this method is submitted by this method + localTxOpen = true + } + + defer func() { + if r := recover(); r != nil { + //err = fmt.Errorf("->事务开启失败:%w ", err) + //记录异常日志 + //if _, ok := r.(runtime.Error); ok { + // panic(r) + //} + var errOk bool + err, errOk = r.(error) + if errOk { + err = fmt.Errorf("->Transaction-->recover异常:%w", err) + FuncLogPanic(ctx, err) + } else { + err = fmt.Errorf("->Transaction-->recover异常:%v", r) + FuncLogPanic(ctx, err) + } + //if !txOpen { //如果不是开启方,也应该回滚事务,虽然可能造成日志不准确,但是回滚要尽早 + // return + //} + //如果禁用了事务 + if getContextBoolValue(ctx, contextDisableTransactionValueKey, dbConnection.config.DisableTransaction) { + return + } + rberr := dbConnection.rollback() + if rberr != nil { + rberr = fmt.Errorf("->Transaction-->recover内事务回滚失败:%w", rberr) + FuncLogError(ctx, rberr) + } + // 任意一个分支事务回滚,分布式事务就整体回滚 + if globalTransaction != nil { + errGlobal = globalTransaction.RollbackGTX(ctx, globalRootContext) + if errGlobal != nil { + errGlobal = fmt.Errorf("->Transaction-->global:recover内globalTransaction事务回滚失败:%w", errGlobal) + FuncLogError(ctx, errGlobal) + } + } + + } + }() + + // 执行业务的事务函数 + info, err = doTransaction(ctx) + + if err != nil { + err = fmt.Errorf("->Transaction-->doTransaction业务执行错误:%w", err) + FuncLogError(ctx, err) + + // 如果禁用了事务 + if getContextBoolValue(ctx, contextDisableTransactionValueKey, dbConnection.config.DisableTransaction) { + return info, err + } + + // 不是开启方回滚事务,有可能造成日志记录不准确,但是回滚最重要了,尽早回滚 + // It is not the start party to roll back the transaction, which may cause inaccurate log records,but rollback is the most important, roll back as soon as possible + errRollback := dbConnection.rollback() + if errRollback != nil { + errRollback = fmt.Errorf("->Transaction-->rollback事务回滚失败:%w", errRollback) + FuncLogError(ctx, errRollback) + } + // 任意一个分支事务回滚,分布式事务就整体回滚 + if globalTransaction != nil { + errGlobal = globalTransaction.RollbackGTX(ctx, globalRootContext) + if errGlobal != nil { + errGlobal = fmt.Errorf("->Transaction-->global:Transaction-->rollback globalTransaction事务回滚失败:%w", errGlobal) + FuncLogError(ctx, errGlobal) + } + } + return info, err + } + // 如果是事务开启方,提交事务 + // If it is the transaction opener, commit the transaction + if localTxOpen { + errCommit := dbConnection.commit() + // 本地事务提交成功,如果是全局事务的开启方,提交分布式事务 + if errCommit == nil && globalTxOpen { + errGlobal = globalTransaction.CommitGTX(ctx, globalRootContext) + if errGlobal != nil { + errGlobal = fmt.Errorf("->Transaction-->global:Transaction-->commit globalTransaction 事务提交失败:%w", errGlobal) + FuncLogError(ctx, errGlobal) + } + } + if errCommit != nil { + errCommit = fmt.Errorf("->Transaction-->commit事务提交失败:%w", errCommit) + FuncLogError(ctx, errCommit) + // 任意一个分支事务回滚,分布式事务就整体回滚 + if globalTransaction != nil { + errGlobal = globalTransaction.RollbackGTX(ctx, globalRootContext) + if errGlobal != nil { + errGlobal = fmt.Errorf("->Transaction-->global:Transaction-->commit失败,然后回滚globalTransaction事务也失败:%w", errGlobal) + FuncLogError(ctx, errGlobal) + } + } + + return info, errCommit + } + + } + + return info, err +} + +var errQueryRow = errors.New("->QueryRow查询出多条数据") + +// QueryRow 不要偷懒调用Query返回第一条,问题1.需要构建一个slice,问题2.调用方传递的对象其他值会被抛弃或者覆盖. +// 只查询一个字段,需要使用这个字段的类型进行接收,目前不支持整个struct对象接收 +// 根据Finder和封装为指定的entity类型,entity必须是*struct类型或者基础类型的指针.把查询的数据赋值给entity,所以要求指针类型 +// context必须传入,不能为空 +// 如果数据库是null,基本类型不支持,会返回异常,不做默认值处理,Query因为是列表,会设置为默认值 +// QueryRow Don't be lazy to call Query to return the first one +// Question 1. A selice needs to be constructed, and question 2. Other values ​​of the object passed by the caller will be discarded or overwritten +// context must be passed in and cannot be empty +func QueryRow(ctx context.Context, finder *Finder, entity interface{}) (bool, error) { + return queryRow(ctx, finder, entity) +} + +var queryRow = func(ctx context.Context, finder *Finder, entity interface{}) (has bool, err error) { + typeOf, errCheck := checkEntityKind(entity) + if errCheck != nil { + errCheck = fmt.Errorf("->QueryRow-->checkEntityKind类型检查错误:%w", errCheck) + FuncLogError(ctx, errCheck) + return has, errCheck + } + // 从contxt中获取数据库连接,可能为nil + // Get database connection from contxt, may be nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + FuncLogError(ctx, errFromContxt) + return has, errFromContxt + } + // 自己构建的dbConnection + // dbConnection built by yourself + if dbConnection != nil && dbConnection.db == nil { + FuncLogError(ctx, errDBConnection) + return has, errDBConnection + } + + config, errConfig := getConfigFromConnection(ctx, dbConnection, 0) + if errConfig != nil { + FuncLogError(ctx, errConfig) + return has, errConfig + } + dialect := config.Dialect + + // 获取到sql语句 + // Get the sql statement + sqlstr, errSQL := wrapQuerySQL(dialect, finder, nil) + if errSQL != nil { + errSQL = fmt.Errorf("->QueryRow-->wrapQuerySQL获取查询SQL语句错误:%w", errSQL) + FuncLogError(ctx, errSQL) + return has, errSQL + } + + // 检查dbConnection.有可能会创建dbConnection或者开启事务,所以要尽可能的接近执行时检查 + // Check db Connection. It is possible to create a db Connection or start a transaction, so check it as close as possible to the execution + var errDbConnection error + ctx, dbConnection, errDbConnection = checkDBConnection(ctx, dbConnection, false, 0) + if errDbConnection != nil { + FuncLogError(ctx, errDbConnection) + return has, errDbConnection + } + + // 根据语句和参数查询 + // Query based on statements and parameters + rows, errQueryContext := dbConnection.queryContext(ctx, &sqlstr, &finder.values) + if errQueryContext != nil { + errQueryContext = fmt.Errorf("->QueryRow-->queryContext查询数据库错误:%w", errQueryContext) + FuncLogError(ctx, errQueryContext) + return has, errQueryContext + } + // 先判断error 再关闭 + defer func() { + // 先判断error 再关闭 + rows.Close() + // 捕获panic,赋值给err,避免程序崩溃 + if r := recover(); r != nil { + has = false + var errOk bool + err, errOk = r.(error) + if errOk { + err = fmt.Errorf("->QueryRow-->recover异常:%w", err) + FuncLogPanic(ctx, err) + } else { + err = fmt.Errorf("->QueryRow-->recover异常:%v", r) + FuncLogPanic(ctx, err) + } + } + }() + + // typeOf := reflect.TypeOf(entity).Elem() + + // 数据库字段类型 + columnTypes, errColumnTypes := rows.ColumnTypes() + if errColumnTypes != nil { + errColumnTypes = fmt.Errorf("->QueryRow-->rows.ColumnTypes数据库类型错误:%w", errColumnTypes) + FuncLogError(ctx, errColumnTypes) + return has, errColumnTypes + } + // 查询的字段长度 + ctLen := len(columnTypes) + // 是否只有一列,而且可以直接赋值 + oneColumnScanner := false + if ctLen < 1 { // 没有返回列 + errColumn0 := errors.New("->QueryRow-->ctLen<1,没有返回列") + FuncLogError(ctx, errColumn0) + return has, errColumn0 + } else if ctLen == 1 { // 如果只查询一个字段 + // 是否是可以直接扫描的类型 + _, oneColumnScanner = entity.(sql.Scanner) + if !oneColumnScanner { + pkgPath := typeOf.PkgPath() + if pkgPath == "" || pkgPath == "time" { // 系统内置变量和time包 + oneColumnScanner = true + } + } + + } + var dbColumnFieldMap map[string]reflect.StructField + var exportFieldMap map[string]reflect.StructField + if !oneColumnScanner { // 如果不是一个直接可以映射的字段,默认为是sturct + // 获取到类型的字段缓存 + // Get the type field cache + dbColumnFieldMap, exportFieldMap, err = getDBColumnExportFieldMap(&typeOf) + if err != nil { + err = fmt.Errorf("->QueryRow-->getDBColumnFieldMap获取字段缓存错误:%w", err) + return has, err + } + } + + // 反射获取 []driver.Value的值,用于处理nil值和自定义类型 + driverValue := reflect.Indirect(reflect.ValueOf(rows)) + driverValue = driverValue.FieldByName("lastcols") + + // 循环遍历结果集 + // Loop through the result set + for i := 0; rows.Next(); i++ { + has = true + if i > 0 { + FuncLogError(ctx, errQueryRow) + return has, errQueryRow + } + if oneColumnScanner { + err = sqlRowsValues(ctx, dialect, nil, &typeOf, rows, &driverValue, columnTypes, entity, &dbColumnFieldMap, &exportFieldMap) + } else { + pv := reflect.ValueOf(entity) + err = sqlRowsValues(ctx, dialect, &pv, &typeOf, rows, &driverValue, columnTypes, nil, &dbColumnFieldMap, &exportFieldMap) + } + + // pv = pv.Elem() + // scan赋值.是一个指针数组,已经根据struct的属性类型初始化了,sql驱动能感知到参数类型,所以可以直接赋值给struct的指针.这样struct的属性就有值了 + // scan assignment. It is an array of pointers that has been initialized according to the attribute type of the struct,The sql driver can perceive the parameter type,so it can be directly assigned to the pointer of the struct. In this way, the attributes of the struct have values + // scanerr := rows.Scan(values...) + if err != nil { + err = fmt.Errorf("->Query-->sqlRowsValues错误:%w", err) + FuncLogError(ctx, err) + return has, err + } + + } + + return has, err +} + +var errQuerySlice = errors.New("->Query数组必须是*[]struct类型或者*[]*struct或者基础类型数组的指针") + +// Query 不要偷懒调用QueryMap,需要处理sql驱动支持的sql.Nullxxx的数据类型,也挺麻烦的 +// 只查询一个字段,需要使用这个字段的类型进行接收,目前不支持整个struct对象接收 +// 根据Finder和封装为指定的entity类型,entity必须是*[]struct类型,已经初始化好的数组,此方法只Append元素,这样调用方就不需要强制类型转换了 +// context必须传入,不能为空.如果想不分页,查询所有数据,page传入nil +// Query:Don't be lazy to call QueryMap, you need to deal with the sql,Nullxxx data type supported by the sql driver, which is also very troublesome +// According to the Finder and encapsulation for the specified entity type, the entity must be of the *[]struct type, which has been initialized,This method only Append elements, so the caller does not need to force type conversion +// context must be passed in and cannot be empty +var Query = func(ctx context.Context, finder *Finder, rowsSlicePtr interface{}, page *Page) error { + return query(ctx, finder, rowsSlicePtr, page) +} + +var query = func(ctx context.Context, finder *Finder, rowsSlicePtr interface{}, page *Page) (err error) { + if rowsSlicePtr == nil { // 如果为nil + FuncLogError(ctx, errQuerySlice) + return errQuerySlice + } + + pvPtr := reflect.ValueOf(rowsSlicePtr) + if pvPtr.Kind() != reflect.Ptr { // 如果不是指针 + FuncLogError(ctx, errQuerySlice) + return errQuerySlice + } + + sliceValue := reflect.Indirect(pvPtr) + + // 如果不是数组 + // If it is not an array. + if sliceValue.Kind() != reflect.Slice { + FuncLogError(ctx, errQuerySlice) + return errQuerySlice + } + // 获取数组内的元素类型 + // Get the element type in the array + sliceElementType := sliceValue.Type().Elem() + + // slice数组里是否是指针,实际参数类似 *[]*struct,兼容这种类型 + sliceElementTypePtr := false + // 如果数组里还是指针类型 + if sliceElementType.Kind() == reflect.Ptr { + sliceElementTypePtr = true + sliceElementType = sliceElementType.Elem() + } + + //如果不是struct + //if !(sliceElementType.Kind() == reflect.Struct || allowBaseTypeMap[sliceElementType.Kind()]) { + // return errors.New("->Query数组必须是*[]struct类型或者*[]*struct或者基础类型数组的指针") + //} + //从contxt中获取数据库连接,可能为nil + //Get database connection from contxt, may be nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + FuncLogError(ctx, errFromContxt) + return errFromContxt + } + // 自己构建的dbConnection + // dbConnection built by yourself + if dbConnection != nil && dbConnection.db == nil { + FuncLogError(ctx, errDBConnection) + return errDBConnection + } + config, errConfig := getConfigFromConnection(ctx, dbConnection, 0) + if errConfig != nil { + FuncLogError(ctx, errConfig) + return errConfig + } + dialect := config.Dialect + + sqlstr, errSQL := wrapQuerySQL(dialect, finder, page) + if errSQL != nil { + errSQL = fmt.Errorf("->Query-->wrapQuerySQL获取查询SQL语句错误:%w", errSQL) + FuncLogError(ctx, errSQL) + return errSQL + } + + // 检查dbConnection.有可能会创建dbConnection或者开启事务,所以要尽可能的接近执行时检查 + // Check db Connection. It is possible to create a db Connection or start a transaction, so check it as close as possible to the execution + var errDbConnection error + ctx, dbConnection, errDbConnection = checkDBConnection(ctx, dbConnection, false, 0) + if errDbConnection != nil { + FuncLogError(ctx, errDbConnection) + return errDbConnection + } + + // 根据语句和参数查询 + // Query based on statements and parameters + rows, errQueryContext := dbConnection.queryContext(ctx, &sqlstr, &finder.values) + if errQueryContext != nil { + errQueryContext = fmt.Errorf("->Query-->queryContext查询rows错误:%w", errQueryContext) + FuncLogError(ctx, errQueryContext) + return errQueryContext + } + // 先判断error 再关闭 + defer func() { + // 先判断error 再关闭 + rows.Close() + // 捕获panic,赋值给err,避免程序崩溃 + if r := recover(); r != nil { + var errOk bool + err, errOk = r.(error) + if errOk { + err = fmt.Errorf("->Query-->recover异常:%w", err) + FuncLogPanic(ctx, err) + } else { + err = fmt.Errorf("->Query-->recover异常:%v", r) + FuncLogPanic(ctx, err) + } + } + }() + + //_, ok := reflect.New(sliceElementType).Interface().(sql.Scanner) + + // 数据库返回的字段类型 + columnTypes, errColumnTypes := rows.ColumnTypes() + if errColumnTypes != nil { + errColumnTypes = fmt.Errorf("->Query-->rows.ColumnTypes数据库类型错误:%w", errColumnTypes) + FuncLogError(ctx, errColumnTypes) + return errColumnTypes + } + // 查询的字段长度 + ctLen := len(columnTypes) + // 是否只有一列,而且可以直接赋值 + oneColumnScanner := false + if ctLen < 1 { // 没有返回列 + errColumn0 := errors.New("->Query-->ctLen<1,没有返回列") + FuncLogError(ctx, errColumn0) + return errColumn0 + } else if ctLen == 1 { // 如果只查询一个字段 + // 是否是可以直接扫描的类型 + _, oneColumnScanner = reflect.New(sliceElementType).Interface().(sql.Scanner) + if !oneColumnScanner { + pkgPath := sliceElementType.PkgPath() + if pkgPath == "" || pkgPath == "time" { // 系统内置变量和time包 + oneColumnScanner = true + } + } + + } + var dbColumnFieldMap map[string]reflect.StructField + var exportFieldMap map[string]reflect.StructField + if !oneColumnScanner { // 如果不是一个直接可以映射的字段,默认为是sturct + // 获取到类型的字段缓存 + // Get the type field cache + dbColumnFieldMap, exportFieldMap, err = getDBColumnExportFieldMap(&sliceElementType) + if err != nil { + err = fmt.Errorf("->Query-->getDBColumnFieldMap获取字段缓存错误:%w", err) + return err + } + } + // 反射获取 []driver.Value的值,用于处理nil值和自定义类型 + driverValue := reflect.Indirect(reflect.ValueOf(rows)) + driverValue = driverValue.FieldByName("lastcols") + // TODO 在这里确定字段直接接收或者struct反射,sqlRowsValues 就不再额外处理了,直接映射数据,提升性能 + // 循环遍历结果集 + // Loop through the result set + for rows.Next() { + pv := reflect.New(sliceElementType) + if oneColumnScanner { + err = sqlRowsValues(ctx, dialect, nil, &sliceElementType, rows, &driverValue, columnTypes, pv.Interface(), &dbColumnFieldMap, &exportFieldMap) + } else { + err = sqlRowsValues(ctx, dialect, &pv, &sliceElementType, rows, &driverValue, columnTypes, nil, &dbColumnFieldMap, &exportFieldMap) + } + + // err = sqlRowsValues(ctx, dialect, &pv, rows, &driverValue, columnTypes, oneColumnScanner, structType, &dbColumnFieldMap, &exportFieldMap) + pv = pv.Elem() + // scan赋值.是一个指针数组,已经根据struct的属性类型初始化了,sql驱动能感知到参数类型,所以可以直接赋值给struct的指针.这样struct的属性就有值了 + // scan assignment. It is an array of pointers that has been initialized according to the attribute type of the struct,The sql driver can perceive the parameter type,so it can be directly assigned to the pointer of the struct. In this way, the attributes of the struct have values + // scanerr := rows.Scan(values...) + if err != nil { + err = fmt.Errorf("->Query-->sqlRowsValues错误:%w", err) + FuncLogError(ctx, err) + return err + } + + // values[i] = f.Addr().Interface() + // 通过反射给slice添加元素 + // Add elements to slice through reflection + if sliceElementTypePtr { // 如果数组里是指针地址,*[]*struct + sliceValue.Set(reflect.Append(sliceValue, pv.Addr())) + } else { + sliceValue.Set(reflect.Append(sliceValue, pv)) + } + + } + + // 查询总条数 + // Query total number + if finder.SelectTotalCount && page != nil { + count, errCount := selectCount(ctx, finder) + if errCount != nil { + errCount = fmt.Errorf("->Query-->selectCount查询总条数错误:%w", errCount) + FuncLogError(ctx, errCount) + return errCount + } + page.setTotalCount(count) + } + + return nil +} + +var ( + errQueryRowMapFinder = errors.New("->QueryRowMap-->finder参数不能为nil") + errQueryRowMapMany = errors.New("->QueryRowMap查询出多条数据") +) + +// QueryRowMap 根据Finder查询,封装Map +// context必须传入,不能为空 +// QueryRowMap encapsulates Map according to Finder query +// context must be passed in and cannot be empty +func QueryRowMap(ctx context.Context, finder *Finder) (map[string]interface{}, error) { + return queryRowMap(ctx, finder) +} + +var queryRowMap = func(ctx context.Context, finder *Finder) (map[string]interface{}, error) { + if finder == nil { + FuncLogError(ctx, errQueryRowMapFinder) + return nil, errQueryRowMapFinder + } + resultMapList, errList := QueryMap(ctx, finder, nil) + if errList != nil { + errList = fmt.Errorf("->QueryRowMap-->QueryMap查询错误:%w", errList) + FuncLogError(ctx, errList) + return nil, errList + } + if resultMapList == nil { + return nil, nil + } + if len(resultMapList) > 1 { + FuncLogError(ctx, errQueryRowMapMany) + return resultMapList[0], errQueryRowMapMany + } else if len(resultMapList) == 0 { // 数据库不存在值 + return nil, nil + } + return resultMapList[0], nil +} + +var errQueryMapFinder = errors.New("->QueryMap-->finder参数不能为nil") + +// QueryMap 根据Finder查询,封装Map数组 +// 根据数据库字段的类型,完成从[]byte到Go类型的映射,理论上其他查询方法都可以调用此方法,但是需要处理sql.Nullxxx等驱动支持的类型 +// context必须传入,不能为空 +// QueryMap According to Finder query, encapsulate Map array +// According to the type of database field, the mapping from []byte to Go type is completed. In theory,other query methods can call this method, but need to deal with types supported by drivers such as sql.Nullxxx +// context must be passed in and cannot be empty +func QueryMap(ctx context.Context, finder *Finder, page *Page) ([]map[string]interface{}, error) { + return queryMap(ctx, finder, page) +} + +var queryMap = func(ctx context.Context, finder *Finder, page *Page) (resultMapList []map[string]interface{}, err error) { + if finder == nil { + FuncLogError(ctx, errQueryMapFinder) + return nil, errQueryMapFinder + } + // 从contxt中获取数据库连接,可能为nil + // Get database connection from contxt, may be nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + FuncLogError(ctx, errFromContxt) + return nil, errFromContxt + } + // 自己构建的dbConnection + // dbConnection built by yourself + if dbConnection != nil && dbConnection.db == nil { + FuncLogError(ctx, errDBConnection) + return nil, errDBConnection + } + + config, errConfig := getConfigFromConnection(ctx, dbConnection, 0) + if errConfig != nil { + FuncLogError(ctx, errConfig) + return nil, errConfig + } + dialect := config.Dialect + sqlstr, errSQL := wrapQuerySQL(dialect, finder, page) + if errSQL != nil { + errSQL = fmt.Errorf("->QueryMap -->wrapQuerySQL查询SQL语句错误:%w", errSQL) + FuncLogError(ctx, errSQL) + return nil, errSQL + } + + // 检查dbConnection.有可能会创建dbConnection或者开启事务,所以要尽可能的接近执行时检查 + // Check db Connection. It is possible to create a db Connection or start a transaction, so check it as close as possible to the execution + var errDbConnection error + ctx, dbConnection, errDbConnection = checkDBConnection(ctx, dbConnection, false, 0) + if errDbConnection != nil { + return nil, errDbConnection + } + + // 根据语句和参数查询 + // Query based on statements and parameters + rows, errQueryContext := dbConnection.queryContext(ctx, &sqlstr, &finder.values) + if errQueryContext != nil { + errQueryContext = fmt.Errorf("->QueryMap-->queryContext查询rows错误:%w", errQueryContext) + FuncLogError(ctx, errQueryContext) + return nil, errQueryContext + } + // 先判断error 再关闭 + defer func() { + // 先判断error 再关闭 + rows.Close() + // 捕获panic,赋值给err,避免程序崩溃 + if r := recover(); r != nil { + var errOk bool + err, errOk = r.(error) + if errOk { + err = fmt.Errorf("->QueryMap-->recover异常:%w", err) + FuncLogPanic(ctx, err) + } else { + err = fmt.Errorf("->QueryMap-->recover异常:%v", r) + FuncLogPanic(ctx, err) + } + } + }() + + // 数据库返回的列类型 + // The types returned by column Type.scan Type are all []byte, use column Type.database Type to judge one by one + columnTypes, errColumnTypes := rows.ColumnTypes() + if errColumnTypes != nil { + errColumnTypes = fmt.Errorf("->QueryMap-->rows.ColumnTypes数据库返回列名错误:%w", errColumnTypes) + FuncLogError(ctx, errColumnTypes) + return nil, errColumnTypes + } + // 反射获取 []driver.Value的值 + driverValue := reflect.Indirect(reflect.ValueOf(rows)) + driverValue = driverValue.FieldByName("lastcols") + resultMapList = make([]map[string]interface{}, 0) + columnTypeLen := len(columnTypes) + // 循环遍历结果集 + // Loop through the result set + for rows.Next() { + // 接收数据库返回的数据,需要使用指针接收 + // To receive the data returned by the database, you need to use the pointer to receive + values := make([]interface{}, columnTypeLen) + // 使用指针类型接收字段值,需要使用interface{}包装一下 + // To use the pointer type to receive the field value, you need to use interface() to wrap it + result := make(map[string]interface{}) + + // 记录需要类型转换的字段信息 + var fieldTempDriverValueMap map[int]*driverValueInfo + if iscdvm { + fieldTempDriverValueMap = make(map[int]*driverValueInfo) + } + + // 给数据赋值初始化变量 + // Initialize variables by assigning values ​​to data + for i, columnType := range columnTypes { + dv := driverValue.Index(i) + if dv.IsValid() && dv.InterfaceData()[0] == 0 { // 该字段的数据库值是null,不再处理,使用默认值 + values[i] = new(interface{}) + continue + } + // 类型转换的接口实现 + var customDriverValueConver ICustomDriverValueConver + // 是否需要类型转换 + var converOK bool = false + // 类型转换的临时值 + var tempDriverValue driver.Value + // 根据接收的类型,获取到类型转换的接口实现,优先匹配指定的数据库类型 + databaseTypeName := strings.ToUpper(columnType.DatabaseTypeName()) + // 判断是否有自定义扩展,避免无意义的反射 + if iscdvm { + customDriverValueConver, converOK = customDriverValueMap[dialect+"."+databaseTypeName] + if !converOK { + customDriverValueConver, converOK = customDriverValueMap[databaseTypeName] + } + } + var errGetDriverValue error + // 如果需要类型转换 + if converOK { + // 获取需要转的临时值 + tempDriverValue, errGetDriverValue = customDriverValueConver.GetDriverValue(ctx, columnType, nil) + if errGetDriverValue != nil { + errGetDriverValue = fmt.Errorf("->QueryMap-->customDriverValueConver.GetDriverValue错误:%w", errGetDriverValue) + FuncLogError(ctx, errGetDriverValue) + return nil, errGetDriverValue + } + // 返回值为nil,不做任何处理,使用原始逻辑 + if tempDriverValue == nil { + values[i] = new(interface{}) + } else { // 如果需要类型转换 + values[i] = tempDriverValue + dvinfo := driverValueInfo{} + dvinfo.customDriverValueConver = customDriverValueConver + dvinfo.columnType = columnType + dvinfo.tempDriverValue = tempDriverValue + fieldTempDriverValueMap[i] = &dvinfo + } + + continue + } + + switch databaseTypeName { + + case "CHAR", "NCHAR", "VARCHAR", "NVARCHAR", "VARCHAR2", "NVARCHAR2", "TINYTEXT", "MEDIUMTEXT", "TEXT", "NTEXT", "LONGTEXT", "LONG", "CHARACTER", "MEMO": + values[i] = new(string) + case "INT", "INT4", "INTEGER", "SERIAL", "SERIAL4", "SERIAL2", "TINYINT", "MEDIUMINT", "SMALLINT", "SMALLSERIAL", "INT2", "VARBIT", "AUTONUMBER": + values[i] = new(int) + case "BIGINT", "BIGSERIAL", "INT8", "SERIAL8": + values[i] = new(int64) + case "FLOAT", "REAL", "FLOAT4", "SINGLE": + values[i] = new(float32) + case "DOUBLE", "FLOAT8": + values[i] = new(float64) + case "DATE", "TIME", "DATETIME", "TIMESTAMP", "TIMESTAMPTZ", "TIMETZ", "INTERVAL", "DATETIME2", "SMALLDATETIME", "DATETIMEOFFSET": + values[i] = new(time.Time) + case "NUMBER": + precision, scale, isDecimal := columnType.DecimalSize() + if isDecimal || precision > 18 || precision-scale > 18 { // 如果是Decimal类型 + values[i] = FuncDecimalValue(ctx, dialect) + } else if scale > 0 { // 有小数位,默认使用float64接收 + values[i] = new(float64) + } else if precision-scale > 9 { // 超过9位,使用int64 + values[i] = new(int64) + } else { // 默认使用int接收 + values[i] = new(int) + } + + case "DECIMAL", "NUMERIC", "DEC": + values[i] = FuncDecimalValue(ctx, dialect) + case "BOOLEAN", "BOOL", "BIT": + values[i] = new(bool) + default: + // 不需要类型转换,正常赋值 + values[i] = new(interface{}) + } + } + // scan赋值 + // scan assignment + errScan := rows.Scan(values...) + if errScan != nil { + errScan = fmt.Errorf("->QueryMap-->rows.Scan错误:%w", errScan) + FuncLogError(ctx, errScan) + return nil, errScan + } + + // 循环 需要类型转换的字段,把临时值赋值给实际的接收对象 + for i, driverValueInfo := range fieldTempDriverValueMap { + // driverValueInfo := *driverValueInfoPtr + // 根据列名,字段类型,新值 返回符合接收类型值的指针,返回值是个指针,指针,指针!!!! + rightValue, errConverDriverValue := driverValueInfo.customDriverValueConver.ConverDriverValue(ctx, driverValueInfo.columnType, driverValueInfo.tempDriverValue, nil) + if errConverDriverValue != nil { + errConverDriverValue = fmt.Errorf("->QueryMap-->customDriverValueConver.ConverDriverValue错误:%w", errConverDriverValue) + FuncLogError(ctx, errConverDriverValue) + return nil, errConverDriverValue + } + // result[driverValueInfo.columnType.Name()] = reflect.ValueOf(rightValue).Elem().Interface() + values[i] = rightValue + } + + // 获取每一列的值 + // Get the value of each column + for i, columnType := range columnTypes { + + // 取到指针下的值,[]byte格式 + // Get the value under the pointer, []byte format + // v := *(values[i].(*interface{})) + v := reflect.ValueOf(values[i]).Elem().Interface() + // 从[]byte转化成实际的类型值,例如string,int + // Convert from []byte to actual type value, such as string, int + // v = converValueColumnType(v, columnType) + // 赋值到Map + // Assign to Map + result[columnType.Name()] = v + + } + + // 添加Map到数组 + // Add Map to the array + resultMapList = append(resultMapList, result) + + } + + // 查询总条数 + // Query total number + if finder.SelectTotalCount && page != nil { + count, errCount := selectCount(ctx, finder) + if errCount != nil { + errCount = fmt.Errorf("->QueryMap-->selectCount查询总条数错误:%w", errCount) + FuncLogError(ctx, errCount) + return resultMapList, errCount + } + page.setTotalCount(count) + } + + return resultMapList, nil +} + +// UpdateFinder 更新Finder语句 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +// UpdateFinder Update Finder statement +// ctx cannot be nil, refer to zorm.Transaction method to pass in ctx. Don't build DB Connection yourself +// The number of rows affected by affected, if it is abnormal or the driver does not support it, return -1 +func UpdateFinder(ctx context.Context, finder *Finder) (int, error) { + return updateFinder(ctx, finder) +} + +var updateFinder = func(ctx context.Context, finder *Finder) (int, error) { + affected := -1 + if finder == nil { + return affected, errors.New("->UpdateFinder-->finder不能为空") + } + sqlstr, err := finder.GetSQL() + if err != nil { + err = fmt.Errorf("->UpdateFinder-->finder.GetSQL()错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + _, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, finder.values, nil) + if errexec != nil { + errexec = fmt.Errorf("->UpdateFinder-->wrapExecUpdateValuesAffected执行更新错误:%w", errexec) + FuncLogError(ctx, errexec) + } + + return affected, errexec +} + +// Insert 保存Struct对象,必须是IEntityStruct类型 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +// Insert saves the Struct object, which must be of type IEntityStruct +// ctx cannot be nil, refer to zorm.Transaction method to pass in ctx. Don't build dbConnection yourself +// The number of rows affected by affected, if it is abnormal or the driver does not support it, return -1 +func Insert(ctx context.Context, entity IEntityStruct) (int, error) { + return insert(ctx, entity) +} + +var insert = func(ctx context.Context, entity IEntityStruct) (int, error) { + affected := -1 + if entity == nil { + return affected, errors.New("->Insert-->entity对象不能为空") + } + typeOf, columns, values, columnAndValueErr := columnAndValue(entity) + if columnAndValueErr != nil { + columnAndValueErr = fmt.Errorf("->Insert-->columnAndValue获取实体类的列和值错误:%w", columnAndValueErr) + FuncLogError(ctx, columnAndValueErr) + return affected, columnAndValueErr + } + if len(columns) < 1 { + return affected, errors.New("->Insert-->没有tag信息,请检查struct中 column 的tag") + } + // 从contxt中获取数据库连接,可能为nil + // Get database connection from contxt, may be nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + return affected, errFromContxt + } + // 自己构建的dbConnection + // dbConnection built by yourself + if dbConnection != nil && dbConnection.db == nil { + return affected, errDBConnection + } + + // SQL语句 + // SQL statement + sqlstr, autoIncrement, pktype, err := wrapInsertSQL(ctx, &typeOf, entity, &columns, &values) + if err != nil { + err = fmt.Errorf("->Insert-->wrapInsertSQL获取保存语句错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + + // oracle 12c+ 支持IDENTITY属性的自增列,因为分页也要求12c+的语法,所以数据库就IDENTITY创建自增吧 + // 处理序列产生的自增主键,例如oracle,postgresql等 + var lastInsertID, zormSQLOutReturningID *int64 + // var zormSQLOutReturningID *int64 + // 如果是postgresql的SERIAL自增,需要使用 RETURNING 返回主键的值 + if autoIncrement > 0 { + config, errConfig := getConfigFromConnection(ctx, dbConnection, 1) + if errConfig != nil { + return affected, errConfig + } + dialect := config.Dialect + lastInsertID, zormSQLOutReturningID = wrapAutoIncrementInsertSQL(entity.GetPKColumnName(), &sqlstr, dialect, &values) + + } + + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + res, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, values, lastInsertID) + if errexec != nil { + errexec = fmt.Errorf("->Insert-->wrapExecUpdateValuesAffected执行保存错误:%w", errexec) + FuncLogError(ctx, errexec) + return affected, errexec + } + + // 如果是自增主键 + // If it is an auto-incrementing primary key + if autoIncrement > 0 { + // 如果是oracle,shentong 的返回自增主键 + if lastInsertID == nil && zormSQLOutReturningID != nil { + lastInsertID = zormSQLOutReturningID + } + + var autoIncrementIDInt64 int64 + var err error + if lastInsertID != nil { + autoIncrementIDInt64 = *lastInsertID + } else { + // 需要数据库支持,获取自增主键 + // Need database support, get auto-incrementing primary key + autoIncrementIDInt64, err = (*res).LastInsertId() + } + + // 数据库不支持自增主键,不再赋值给struct属性 + // The database does not support self-incrementing primary keys, and no longer assigns values ​​to struct attributes + if err != nil { + err = fmt.Errorf("->Insert-->LastInsertId数据库不支持自增主键,不再赋值给struct属性:%w", err) + FuncLogError(ctx, err) + return affected, nil + } + pkName := entity.GetPKColumnName() + if pktype == "int" { + // int64 转 int + // int64 to int + autoIncrementIDInt, _ := typeConvertInt64toInt(autoIncrementIDInt64) + // 设置自增主键的值 + // Set the value of the auto-incrementing primary key + err = setFieldValueByColumnName(entity, pkName, autoIncrementIDInt) + } else if pktype == "int64" { + // 设置自增主键的值 + // Set the value of the auto-incrementing primary key + err = setFieldValueByColumnName(entity, pkName, autoIncrementIDInt64) + } + + if err != nil { + err = fmt.Errorf("->Insert-->setFieldValueByColumnName反射赋值数据库返回的自增主键错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + } + + return affected, nil +} + +// InsertSlice 批量保存Struct Slice 数组对象,必须是[]IEntityStruct类型,使用IEntityStruct接口,兼容Struct实体类 +// 如果是自增主键,无法对Struct对象里的主键属性赋值 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +func InsertSlice(ctx context.Context, entityStructSlice []IEntityStruct) (int, error) { + return insertSlice(ctx, entityStructSlice) +} + +var insertSlice = func(ctx context.Context, entityStructSlice []IEntityStruct) (int, error) { + affected := -1 + if entityStructSlice == nil || len(entityStructSlice) < 1 { + return affected, errors.New("->InsertSlice-->entityStructSlice对象数组不能为空") + } + // 第一个对象,获取第一个Struct对象,用于获取数据库字段,也获取了值 + entity := entityStructSlice[0] + typeOf, columns, values, columnAndValueErr := columnAndValue(entity) + if columnAndValueErr != nil { + columnAndValueErr = fmt.Errorf("->InsertSlice-->columnAndValue获取实体类的列和值错误:%w", columnAndValueErr) + FuncLogError(ctx, columnAndValueErr) + return affected, columnAndValueErr + } + if len(columns) < 1 { + return affected, errors.New("->InsertSlice-->columns没有tag信息,请检查struct中 column 的tag") + } + // 从contxt中获取数据库连接,可能为nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + return affected, errFromContxt + } + // 自己构建的dbConnection + if dbConnection != nil && dbConnection.db == nil { + return affected, errDBConnection + } + config, errConfig := getConfigFromConnection(ctx, dbConnection, 1) + if errConfig != nil { + return affected, errConfig + } + // SQL语句 + sqlstr, _, err := wrapInsertSliceSQL(ctx, config, &typeOf, entityStructSlice, &columns, &values) + if err != nil { + err = fmt.Errorf("->InsertSlice-->wrapInsertSliceSQL获取保存语句错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + _, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, values, nil) + if errexec != nil { + errexec = fmt.Errorf("->InsertSlice-->wrapExecUpdateValuesAffected执行保存错误:%w", errexec) + FuncLogError(ctx, errexec) + } + + return affected, errexec +} + +// Update 更新struct所有属性,必须是IEntityStruct类型 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +func Update(ctx context.Context, entity IEntityStruct) (int, error) { + return update(ctx, entity) +} + +var update = func(ctx context.Context, entity IEntityStruct) (int, error) { + finder, err := WrapUpdateStructFinder(ctx, entity, false) + if err != nil { + err = fmt.Errorf("->Update-->WrapUpdateStructFinder包装Finder错误:%w", err) + FuncLogError(ctx, err) + return 0, err + } + return UpdateFinder(ctx, finder) +} + +// UpdateNotZeroValue 更新struct不为默认零值的属性,必须是IEntityStruct类型,主键必须有值 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +func UpdateNotZeroValue(ctx context.Context, entity IEntityStruct) (int, error) { + return updateNotZeroValue(ctx, entity) +} + +var updateNotZeroValue = func(ctx context.Context, entity IEntityStruct) (int, error) { + finder, err := WrapUpdateStructFinder(ctx, entity, true) + if err != nil { + err = fmt.Errorf("->UpdateNotZeroValue-->WrapUpdateStructFinder包装Finder错误:%w", err) + FuncLogError(ctx, err) + return 0, err + } + return UpdateFinder(ctx, finder) +} + +// Delete 根据主键删除一个对象.必须是IEntityStruct类型 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +func Delete(ctx context.Context, entity IEntityStruct) (int, error) { + return delete(ctx, entity) +} + +var delete = func(ctx context.Context, entity IEntityStruct) (int, error) { + affected := -1 + typeOf, checkerr := checkEntityKind(entity) + if checkerr != nil { + return affected, checkerr + } + + pkName, pkNameErr := entityPKFieldName(entity, &typeOf) + + if pkNameErr != nil { + pkNameErr = fmt.Errorf("->Delete-->entityPKFieldName获取主键名称错误:%w", pkNameErr) + FuncLogError(ctx, pkNameErr) + return affected, pkNameErr + } + + value, e := structFieldValue(entity, pkName) + if e != nil { + e = fmt.Errorf("->Delete-->structFieldValue获取主键值错误:%w", e) + FuncLogError(ctx, e) + return affected, e + } + + // SQL语句 + sqlstr, err := wrapDeleteSQL(entity) + if err != nil { + err = fmt.Errorf("->Delete-->wrapDeleteSQL获取SQL语句错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + values := make([]interface{}, 1) + values[0] = value + _, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, values, nil) + if errexec != nil { + errexec = fmt.Errorf("->Delete-->wrapExecUpdateValuesAffected执行删除错误:%w", errexec) + FuncLogError(ctx, errexec) + } + + return affected, errexec +} + +// InsertEntityMap 保存*IEntityMap对象.使用Map保存数据,用于不方便使用struct的场景,如果主键是自增或者序列,不要entityMap.Set主键的值 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +func InsertEntityMap(ctx context.Context, entity IEntityMap) (int, error) { + return insertEntityMap(ctx, entity) +} + +var insertEntityMap = func(ctx context.Context, entity IEntityMap) (int, error) { + affected := -1 + // 检查是否是指针对象 + _, checkerr := checkEntityKind(entity) + if checkerr != nil { + return affected, checkerr + } + + // 从contxt中获取数据库连接,可能为nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + return affected, errFromContxt + } + + // 自己构建的dbConnection + if dbConnection != nil && dbConnection.db == nil { + return affected, errDBConnection + } + + // SQL语句 + sqlstr, values, autoIncrement, err := wrapInsertEntityMapSQL(entity) + if err != nil { + err = fmt.Errorf("->InsertEntityMap-->wrapInsertEntityMapSQL获取SQL语句错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + + // 处理序列产生的自增主键,例如oracle,postgresql等 + var lastInsertID, zormSQLOutReturningID *int64 + // 如果是postgresql的SERIAL自增,需要使用 RETURNING 返回主键的值 + if autoIncrement && entity.GetPKColumnName() != "" { + config, errConfig := getConfigFromConnection(ctx, dbConnection, 1) + if errConfig != nil { + return affected, errConfig + } + dialect := config.Dialect + lastInsertID, zormSQLOutReturningID = wrapAutoIncrementInsertSQL(entity.GetPKColumnName(), &sqlstr, dialect, &values) + } + + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + res, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, values, lastInsertID) + if errexec != nil { + errexec = fmt.Errorf("->InsertEntityMap-->wrapExecUpdateValuesAffected执行保存错误:%w", errexec) + FuncLogError(ctx, errexec) + return affected, errexec + } + + // 如果是自增主键 + if autoIncrement { + // 如果是oracle,shentong 的返回自增主键 + if lastInsertID == nil && zormSQLOutReturningID != nil { + lastInsertID = zormSQLOutReturningID + } + + var autoIncrementIDInt64 int64 + var e error + if lastInsertID != nil { + autoIncrementIDInt64 = *lastInsertID + } else { + // 需要数据库支持,获取自增主键 + // Need database support, get auto-incrementing primary key + autoIncrementIDInt64, e = (*res).LastInsertId() + } + if e != nil { // 数据库不支持自增主键,不再赋值给struct属性 + e = fmt.Errorf("->InsertEntityMap数据库不支持自增主键,不再赋值给IEntityMap:%w", e) + FuncLogError(ctx, e) + return affected, nil + } + // int64 转 int + strInt64 := strconv.FormatInt(autoIncrementIDInt64, 10) + autoIncrementIDInt, _ := strconv.Atoi(strInt64) + // 设置自增主键的值 + entity.Set(entity.GetPKColumnName(), autoIncrementIDInt) + } + + return affected, nil +} + +// InsertEntityMapSlice 保存[]IEntityMap对象.使用Map保存数据,用于不方便使用struct的场景,如果主键是自增或者序列,不要entityMap.Set主键的值 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +func InsertEntityMapSlice(ctx context.Context, entityMapSlice []IEntityMap) (int, error) { + return insertEntityMapSlice(ctx, entityMapSlice) +} + +var insertEntityMapSlice = func(ctx context.Context, entityMapSlice []IEntityMap) (int, error) { + affected := -1 + // 从contxt中获取数据库连接,可能为nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + return affected, errFromContxt + } + // 自己构建的dbConnection + if dbConnection != nil && dbConnection.db == nil { + return affected, errDBConnection + } + config, errConfig := getConfigFromConnection(ctx, dbConnection, 1) + if errConfig != nil { + return affected, errConfig + } + // SQL语句 + sqlstr, values, err := wrapInsertEntityMapSliceSQL(ctx, config, entityMapSlice) + if err != nil { + err = fmt.Errorf("->InsertEntityMapSlice-->wrapInsertEntityMapSliceSQL获取SQL语句错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + _, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, values, nil) + if errexec != nil { + errexec = fmt.Errorf("->InsertEntityMapSlice-->wrapExecUpdateValuesAffected执行保存错误:%w", errexec) + FuncLogError(ctx, errexec) + return affected, errexec + } + return affected, errexec +} + +// UpdateEntityMap 更新IEntityMap对象.用于不方便使用struct的场景,主键必须有值 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// affected影响的行数,如果异常或者驱动不支持,返回-1 +// UpdateEntityMap Update IEntityMap object. Used in scenarios where struct is not convenient, the primary key must have a value +// ctx cannot be nil, refer to zorm.Transaction method to pass in ctx. Don't build DB Connection yourself +// The number of rows affected by "affected", if it is abnormal or the driver does not support it, return -1 +func UpdateEntityMap(ctx context.Context, entity IEntityMap) (int, error) { + return updateEntityMap(ctx, entity) +} + +var updateEntityMap = func(ctx context.Context, entity IEntityMap) (int, error) { + affected := -1 + // 检查是否是指针对象 + // Check if it is a pointer + _, checkerr := checkEntityKind(entity) + if checkerr != nil { + return affected, checkerr + } + // 从contxt中获取数据库连接,可能为nil + // Get database connection from contxt, it may be nil + dbConnection, errFromContxt := getDBConnectionFromContext(ctx) + if errFromContxt != nil { + return affected, errFromContxt + } + // 自己构建的dbConnection + // dbConnection built by yourself + if dbConnection != nil && dbConnection.db == nil { + return affected, errDBConnection + } + + // SQL语句 + // SQL statement + sqlstr, values, err := wrapUpdateEntityMapSQL(entity) + if err != nil { + err = fmt.Errorf("->UpdateEntityMap-->wrapUpdateEntityMapSQL获取SQL语句错误:%w", err) + FuncLogError(ctx, err) + return affected, err + } + // 包装update执行,赋值给影响的函数指针变量,返回*sql.Result + _, errexec := wrapExecUpdateValuesAffected(ctx, &affected, &sqlstr, values, nil) + if errexec != nil { + errexec = fmt.Errorf("->UpdateEntityMap-->wrapExecUpdateValuesAffected执行更新错误:%w", errexec) + FuncLogError(ctx, errexec) + } + + return affected, errexec +} + +// IsInTransaction 检查ctx是否包含事务 +func IsInTransaction(ctx context.Context) (bool, error) { + dbConnection, err := getDBConnectionFromContext(ctx) + if err != nil { + return false, err + } + if dbConnection != nil && dbConnection.tx != nil { + return true, err + } + return false, err +} + +// WrapUpdateStructFinder 返回更新IEntityStruct的Finder对象 +// ctx不能为nil,参照使用zorm.Transaction方法传入ctx.也不要自己构建DBConnection +// Finder为更新执行的Finder,更新语句统一使用Finder执行 +// updateStructFunc Update object +// ctx cannot be nil, refer to zorm.Transaction method to pass in ctx. Don't build DB Connection yourself +// Finder is the Finder that executes the update, and the update statement is executed uniformly using the Finder +func WrapUpdateStructFinder(ctx context.Context, entity IEntityStruct, onlyUpdateNotZero bool) (*Finder, error) { + // affected := -1 + if entity == nil { + return nil, errors.New("->WrapUpdateStructFinder-->entity对象不能为空") + } + + typeOf, columns, values, columnAndValueErr := columnAndValue(entity) + if columnAndValueErr != nil { + return nil, columnAndValueErr + } + + // SQL语句 + // SQL statement + sqlstr, err := wrapUpdateSQL(&typeOf, entity, &columns, &values, onlyUpdateNotZero) + if err != nil { + return nil, err + } + // finder对象 + finder := NewFinder() + finder.sqlstr = sqlstr + finder.sqlBuilder.WriteString(sqlstr) + finder.values = values + return finder, nil +} + +// selectCount 根据finder查询总条数 +// context必须传入,不能为空 +// selectCount Query the total number of items according to finder +// context must be passed in and cannot be empty +func selectCount(ctx context.Context, finder *Finder) (int, error) { + if finder == nil { + return -1, errors.New("->selectCount-->finder参数为nil") + } + // 自定义的查询总条数Finder,主要是为了在group by等复杂情况下,为了性能,手动编写总条数语句 + // Customized query total number Finder,mainly for the sake of performance in complex situations such as group by, manually write the total number of statements + if finder.CountFinder != nil { + count := -1 + _, err := QueryRow(ctx, finder.CountFinder, &count) + if err != nil { + return -1, err + } + return count, nil + } + + countsql, counterr := finder.GetSQL() + if counterr != nil { + return -1, counterr + } + + // 查询order by 的位置 + // Query the position of order by + locOrderBy := findOrderByIndex(&countsql) + // 如果存在order by + // If there is order by + if len(locOrderBy) > 0 { + countsql = countsql[:locOrderBy[0]] + } + s := strings.ToLower(countsql) + gbi := -1 + locGroupBy := findGroupByIndex(&countsql) + if len(locGroupBy) > 0 { + gbi = locGroupBy[0] + } + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + // 特殊关键字,包装SQL + // Special keywords, wrap SQL + if strings.Contains(s, " distinct ") || strings.Contains(s, " union ") || gbi > -1 { + // countsql = "SELECT COUNT(*) frame_row_count FROM (" + countsql + ") temp_frame_noob_table_name WHERE 1=1 " + sqlBuilder.WriteString("SELECT COUNT(*) frame_row_count FROM (") + sqlBuilder.WriteString(countsql) + sqlBuilder.WriteString(") temp_frame_noob_table_name WHERE 1=1 ") + } else { + locFrom := findSelectFromIndex(&countsql) + // 没有找到FROM关键字,认为是异常语句 + // The FROM keyword was not found, which is considered an abnormal statement + if len(locFrom) == 0 { + return -1, errors.New("->selectCount-->findFromIndex没有FROM关键字,语句错误") + } + // countsql = "SELECT COUNT(*) " + countsql[locFrom[0]:] + sqlBuilder.WriteString("SELECT COUNT(*) ") + sqlBuilder.WriteString(countsql[locFrom[0]:]) + } + countsql = sqlBuilder.String() + countFinder := NewFinder() + countFinder.Append(countsql) + countFinder.values = finder.values + + count := -1 + _, cerr := QueryRow(ctx, countFinder, &count) + if cerr != nil { + return -1, cerr + } + return count, nil +} + +// getDBConnectionFromContext 从Conext中获取数据库连接 +// getDBConnectionFromContext Get database connection from Conext +func getDBConnectionFromContext(ctx context.Context) (*dataBaseConnection, error) { + if ctx == nil { + return nil, errors.New("->getDBConnectionFromContext-->context不能为空") + } + // 获取数据库连接 + // Get database connection + value := ctx.Value(contextDBConnectionValueKey) + if value == nil { + return nil, nil + } + dbConnection, isdb := value.(*dataBaseConnection) + if !isdb { // 不是数据库连接 + return nil, errors.New("->getDBConnectionFromContext-->context传递了错误的*DBConnection类型值") + } + return dbConnection, nil +} + +// 变量名建议errFoo这样的驼峰 +// The variable name suggests a hump like "errFoo" +var errDBConnection = errors.New("更新操作需要使用zorm.Transaction开启事务.读取操作如果ctx没有dbConnection,使用FuncReadWriteStrategy(ctx,rwType).newDBConnection(),如果dbConnection有事务,就使用事务查询") + +// checkDBConnection 检查dbConnection.有可能会创建dbConnection或者开启事务,所以要尽可能的接近执行时检查 +// context必须传入,不能为空.rwType=0 read,rwType=1 write +// checkDBConnection It is possible to create a db Connection or open a transaction, so check it as close as possible to execution +// The context must be passed in and cannot be empty. rwType=0 read, rwType=1 write +func checkDBConnection(ctx context.Context, dbConnection *dataBaseConnection, hastx bool, rwType int) (context.Context, *dataBaseConnection, error) { + var errFromContext error + if dbConnection == nil { + dbConnection, errFromContext = getDBConnectionFromContext(ctx) + if errFromContext != nil { + return ctx, nil, errFromContext + } + } + + // dbConnection为空 + // dbConnection is nil + if dbConnection == nil { + dbdao, err := FuncReadWriteStrategy(ctx, rwType) + if err != nil { + return ctx, nil, err + } + // 是否禁用了事务 + disabletx := getContextBoolValue(ctx, contextDisableTransactionValueKey, dbdao.config.DisableTransaction) + // 如果要求有事务,事务需要手动zorm.Transaction显示开启.如果自动开启,就会为了偷懒,每个操作都自动开启,事务就失去意义了 + if hastx && (!disabletx) { + // if hastx { + return ctx, nil, errDBConnection + } + + // 如果要求没有事务,实例化一个默认的dbConnection + // If no transaction is required, instantiate a default db Connection + var errGetDBConnection error + + dbConnection, errGetDBConnection = dbdao.newDBConnection() + if errGetDBConnection != nil { + return ctx, nil, errGetDBConnection + } + // 把dbConnection放入context + // Put db Connection into context + ctx = context.WithValue(ctx, contextDBConnectionValueKey, dbConnection) + + } else { // 如果dbConnection存在 + // If db Connection exists + if dbConnection.db == nil { // 禁止外部构建 + return ctx, dbConnection, errDBConnection + } + if dbConnection.tx == nil && hastx && (!getContextBoolValue(ctx, contextDisableTransactionValueKey, dbConnection.config.DisableTransaction)) { + // if dbConnection.tx == nil && hastx { //如果要求有事务,事务需要手动zorm.Transaction显示开启.如果自动开启,就会为了偷懒,每个操作都自动开启,事务就失去意义了 + return ctx, dbConnection, errDBConnection + } + } + return ctx, dbConnection, nil +} + +// wrapExecUpdateValuesAffected 包装update执行,赋值给影响的函数指针变量,返回*sql.Result +func wrapExecUpdateValuesAffected(ctx context.Context, affected *int, sqlstrptr *string, values []interface{}, lastInsertID *int64) (*sql.Result, error) { + // 必须要有dbConnection和事务.有可能会创建dbConnection放入ctx或者开启事务,所以要尽可能的接近执行时检查 + // There must be a db Connection and transaction.It is possible to create a db Connection into ctx or open a transaction, so check as close as possible to the execution + var dbConnectionerr error + var dbConnection *dataBaseConnection + ctx, dbConnection, dbConnectionerr = checkDBConnection(ctx, dbConnection, true, 1) + if dbConnectionerr != nil { + return nil, dbConnectionerr + } + + var res *sql.Result + var errexec error + if lastInsertID != nil { + sqlrow, errrow := dbConnection.queryRowContext(ctx, sqlstrptr, &values) + if errrow != nil { + return res, errrow + } + errexec = sqlrow.Scan(lastInsertID) + if errexec == nil { // 如果插入成功,返回 + *affected = 1 + return res, errexec + } + } else { + res, errexec = dbConnection.execContext(ctx, sqlstrptr, &values) + } + + if errexec != nil { + return res, errexec + } + // 影响的行数 + // Number of rows affected + + rowsAffected, errAffected := (*res).RowsAffected() + if errAffected == nil { + *affected, errAffected = typeConvertInt64toInt(rowsAffected) + } else { // 如果不支持返回条数,设置位nil,影响的条数设置成-1 + *affected = -1 + errAffected = nil + } + return res, errAffected +} + +// contextSQLHintValueKey 把sql hint放到context里使用的key +const contextSQLHintValueKey = wrapContextStringKey("contextSQLHintValueKey") + +// BindContextSQLHint context中绑定sql的hint,使用这个Context的方法都会传播hint传播的语句 +// hint 是完整的sql片段, 例如: hint:="/*+ XID('gs/aggregationSvc/2612341069705662465') */" +func BindContextSQLHint(parent context.Context, hint string) (context.Context, error) { + if parent == nil { + return nil, errors.New("->BindContextSQLHint-->context的parent不能为nil") + } + if hint == "" { + return nil, errors.New("->BindContextSQLHint-->hint不能为空") + } + + ctx := context.WithValue(parent, contextSQLHintValueKey, hint) + return ctx, nil +} + +// contextEnableGlobalTransactionValueKey 是否使用分布式事务放到context里使用的key +const contextEnableGlobalTransactionValueKey = wrapContextStringKey("contextEnableGlobalTransactionValueKey") + +// BindContextEnableGlobalTransaction context启用分布式事务,不再自动设置,必须手动启用分布式事务,必须放到本地事务开启之前调用 +func BindContextEnableGlobalTransaction(parent context.Context) (context.Context, error) { + if parent == nil { + return nil, errors.New("->BindContextEnableGlobalTransaction-->context的parent不能为nil") + } + ctx := context.WithValue(parent, contextEnableGlobalTransactionValueKey, true) + return ctx, nil +} + +// contextDisableTransactionValueKey 是否禁用事务放到context里使用的key +const contextDisableTransactionValueKey = wrapContextStringKey("contextDisableTransactionValueKey") + +// BindContextDisableTransaction context禁用事务,必须放到事务开启之前调用.用在不使用事务更新数据库的场景,强烈建议不要使用这个方法,更新数据库必须有事务!!! +func BindContextDisableTransaction(parent context.Context) (context.Context, error) { + if parent == nil { + return nil, errors.New("->BindContextDisableTransaction-->context的parent不能为nil") + } + ctx := context.WithValue(parent, contextDisableTransactionValueKey, true) + return ctx, nil +} + +// getContextBoolValue 从ctx中获取key的bool值,ctx如果没有值使用defaultValue +func getContextBoolValue(ctx context.Context, key wrapContextStringKey, defaultValue bool) bool { + boolValue := false + ctxBoolValue := ctx.Value(key) + if ctxBoolValue != nil { // 如果有值 + boolValue = ctxBoolValue.(bool) + } else { // ctx如果没有值使用defaultValue + boolValue = defaultValue + } + return boolValue +} diff --git a/vendor/gitee.com/chunanyong/zorm/Finder.go b/vendor/gitee.com/chunanyong/zorm/Finder.go new file mode 100644 index 00000000..1aae23c0 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/Finder.go @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "errors" + "strings" +) + +// Finder 查询数据库的载体,所有的sql语句都要通过Finder执行. +// Finder To query the database carrier, all SQL statements must be executed through Finder +type Finder struct { + // 拼接SQL + // Splicing SQL. + sqlBuilder strings.Builder + // SQL的参数值 + // SQL parameter values. + values []interface{} + // 注入检查,默认true 不允许SQL注入的 ' 单引号 + // Injection check, default true does not allow SQL injection single quote + InjectionCheck bool + // CountFinder 自定义的查询总条数'Finder',使用指针默认为nil.主要是为了在'group by'等复杂情况下,为了性能,手动编写总条数语句 + // CountFinder The total number of custom queries is'Finder', and the pointer is nil by default. It is mainly used to manually write the total number of statements for performance in complex situations such as'group by' + CountFinder *Finder + // 是否自动查询总条数,默认true.同时需要Page不为nil,才查询总条数 + // Whether to automatically query the total number of entries, the default is true. At the same time, the Page is not nil to query the total number of entries + SelectTotalCount bool + // SQL语句 + // SQL statement + sqlstr string +} + +// NewFinder 初始化一个Finder,生成一个空的Finder +// NewFinder Initialize a Finder and generate an empty Finder +func NewFinder() *Finder { + finder := Finder{} + finder.sqlBuilder.Grow(stringBuilderGrowLen) + finder.SelectTotalCount = true + finder.InjectionCheck = true + // slice扩容会生成新的slice,最后要值复制接收.问:为什么cap是3?答:经验 + finder.values = make([]interface{}, 0, 3) + return &finder +} + +// NewSelectFinder 根据表名初始化查询的Finder,strs 只取第一个字符串,用数组类型是为了可以不传入,默认为 * | Finder that initializes the query based on the table name +// NewSelectFinder("tableName") SELECT * FROM tableName +// NewSelectFinder("tableName", "id,name") SELECT id,name FROM tableName +func NewSelectFinder(tableName string, strs ...string) *Finder { + strsLen := len(strs) + if strsLen > 1 { // 不支持多个参数 + return nil + } + finder := NewFinder() + finder.sqlBuilder.WriteString("SELECT ") + if strsLen == 1 { // 只取值第一个字符串 + finder.sqlBuilder.WriteString(strs[0]) + } else { + finder.sqlBuilder.WriteByte('*') + } + finder.sqlBuilder.WriteString(" FROM ") + finder.sqlBuilder.WriteString(tableName) + return finder +} + +// NewUpdateFinder 根据表名初始化更新的Finder, UPDATE tableName SET +// NewUpdateFinder Initialize the updated Finder according to the table name, UPDATE tableName SET +func NewUpdateFinder(tableName string) *Finder { + finder := NewFinder() + finder.sqlBuilder.WriteString("UPDATE ") + finder.sqlBuilder.WriteString(tableName) + finder.sqlBuilder.WriteString(" SET ") + return finder +} + +// NewDeleteFinder 根据表名初始化删除的'Finder', DELETE FROM tableName +// NewDeleteFinder Finder for initial deletion based on table name. DELETE FROM tableName +func NewDeleteFinder(tableName string) *Finder { + finder := NewFinder() + finder.sqlBuilder.WriteString("DELETE FROM ") + finder.sqlBuilder.WriteString(tableName) + // 所有的 WHERE 都不加,规则统一,好记 + // No WHERE is added, the rules are unified, easy to remember + // finder.sqlBuilder.WriteString(" WHERE ") + return finder +} + +// Append 添加SQL和参数的值,第一个参数是语句,后面的参数[可选]是参数的值,顺序要正确 +// 例如: finder.Append(" and id=? and name=? ",23123,"abc") +// 只拼接SQL,例如: finder.Append(" and name=123 ") +// Append:Add SQL and parameter values, the first parameter is the statement, and the following parameter (optional) is the value of the parameter, in the correct order +// E.g: finder.Append(" and id=? and name=? ",23123,"abc") +// Only splice SQL, E.g: finder.Append(" and name=123 ") +func (finder *Finder) Append(s string, values ...interface{}) *Finder { + // 不要自己构建finder,使用NewFinder()方法 + // Don't build finder by yourself, use NewFinder() method + if finder == nil || finder.values == nil { + return nil + } + + if s != "" { + if finder.sqlstr != "" { + finder.sqlstr = "" + } + // 默认加一个空格,避免手误两个字符串连接再一起 + // A space is added by default to avoid hand mistakes when connecting two strings together + finder.sqlBuilder.WriteByte(' ') + + finder.sqlBuilder.WriteString(s) + + } + if values == nil || len(values) < 1 { + return finder + } + + finder.values = append(finder.values, values...) + return finder +} + +// AppendFinder 添加另一个Finder finder.AppendFinder(f) +// AppendFinder Add another Finder . finder.AppendFinder(f) +func (finder *Finder) AppendFinder(f *Finder) (*Finder, error) { + if finder == nil { + return finder, errors.New("->finder-->AppendFinder()finder对象为nil") + } + if f == nil { + return finder, errors.New("->finder-->AppendFinder()参数是nil") + } + + // 不要自己构建finder,使用NewFinder()方法 + // Don't build finder by yourself, use NewFinder() method + if finder.values == nil { + return finder, errors.New("->finder-->AppendFinder()不要自己构建finder,使用NewFinder()方法") + } + + // 添加f的SQL + // SQL to add f + sqlstr, err := f.GetSQL() + if err != nil { + return finder, err + } + finder.sqlstr = "" + finder.sqlBuilder.WriteString(sqlstr) + // 添加f的值 + // Add the value of f + finder.values = append(finder.values, f.values...) + return finder, nil +} + +// GetSQL 返回Finder封装的SQL语句 +// GetSQL Return the SQL statement encapsulated by the Finder +func (finder *Finder) GetSQL() (string, error) { + // 不要自己构建finder,使用NewFinder方法 + // Don't build finder by yourself, use NewFinder method + if finder == nil || finder.values == nil { + return "", errors.New("->finder-->GetSQL()不要自己构建finder,使用NewFinder()方法") + } + if len(finder.sqlstr) > 0 { + return finder.sqlstr, nil + } + sqlstr := finder.sqlBuilder.String() + // 包含单引号,属于非法字符串 + // Contains single quotes, which are illegal strings + if finder.InjectionCheck && (strings.Contains(sqlstr, "'")) { + return "", errors.New(`->finder-->GetSQL()SQL语句请不要直接拼接字符串参数,容易注入!!!请使用问号占位符,例如 finder.Append("and id=?","stringId"),如果必须拼接字符串,请设置 finder.InjectionCheck = false `) + } + finder.sqlstr = sqlstr + return sqlstr, nil +} diff --git a/vendor/gitee.com/chunanyong/zorm/ICustomDriverValueConver.go b/vendor/gitee.com/chunanyong/zorm/ICustomDriverValueConver.go new file mode 100644 index 00000000..c0373884 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/ICustomDriverValueConver.go @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "reflect" + "strings" +) + +// customDriverValueMap 用于配置数据库字段类型的处理关系,key是 Dialect.字段类型,例如 dm.TEXT +var customDriverValueMap = make(map[string]ICustomDriverValueConver) + +// iscdvm 是否有自定义的DriverValueMap +var iscdvm bool + +// ICustomDriverValueConver 自定义类型转化接口,用于解决 类似达梦 text --> dm.DmClob --> string类型接收的问题 +type ICustomDriverValueConver interface { + // GetDriverValue 根据数据库列类型,返回driver.Value的实例,struct属性类型 + // map接收或者字段不存在,无法获取到structFieldType,会传入nil + GetDriverValue(ctx context.Context, columnType *sql.ColumnType, structFieldType *reflect.Type) (driver.Value, error) + + // ConverDriverValue 数据库列类型,GetDriverValue返回的driver.Value的临时接收值,struct属性类型 + // map接收或者字段不存在,无法获取到structFieldType,会传入nil + // 返回符合接收类型值的指针,指针,指针!!!! + ConverDriverValue(ctx context.Context, columnType *sql.ColumnType, tempDriverValue driver.Value, structFieldType *reflect.Type) (interface{}, error) +} + +// RegisterCustomDriverValueConver 注册自定义的字段处理逻辑,用于驱动无法直接转换的场景,例如达梦的 TEXT 无法直接转化成 string +// dialectColumnType 值是 Dialect.字段类型,例如: dm.TEXT +// 一般是放到init方法里进行注册 +func RegisterCustomDriverValueConver(dialectColumnType string, customDriverValueConver ICustomDriverValueConver) error { + if len(dialectColumnType) < 1 { + return errors.New("->RegisterCustomDriverValueConver-->dialectColumnType为空") + } + dialectColumnTypes := strings.Split(dialectColumnType, ".") + if len(dialectColumnTypes) < 2 { + customDriverValueMap[strings.ToUpper(dialectColumnType)] = customDriverValueConver + err := errors.New("->RegisterCustomDriverValueConver-->dialectColumnType 值是 Dialect.字段类型,例如: dm.TEXT ,本次正常运行,请尽快修改") + FuncLogError(nil, err) + } else { + customDriverValueMap[strings.ToLower(dialectColumnTypes[0])+"."+strings.ToUpper(dialectColumnTypes[1])] = customDriverValueConver + } + iscdvm = true + return nil +} + +type driverValueInfo struct { + customDriverValueConver ICustomDriverValueConver + columnType *sql.ColumnType + tempDriverValue interface{} + structFieldType *reflect.Type +} + +/** + +import ( + // 00.引入数据库驱动 + "gitee.com/chunanyong/dm" + "io" +) + +// CustomDMText 实现ICustomDriverValueConver接口,扩展自定义类型,例如 达梦数据库TEXT类型,映射出来的是dm.DmClob类型,无法使用string类型直接接收 +type CustomDMText struct{} + +// GetDriverValue 根据数据库列类型,返回driver.Value的实例,struct属性类型 +// map接收或者字段不存在,无法获取到structFieldType,会传入nil +func (dmtext CustomDMText) GetDriverValue(ctx context.Context, columnType *sql.ColumnType, structFieldType *reflect.Type) (driver.Value, error) { + // 如果需要使用structFieldType,需要先判断是否为nil + // if structFieldType != nil { + // } + + return &dm.DmClob{}, nil +} + +// ConverDriverValue 数据库列类型,GetDriverValue返回的driver.Value的临时接收值,struct属性类型 +// map接收或者字段不存在,无法获取到structFieldType,会传入nil +// 返回符合接收类型值的指针,指针,指针!!!! +func (dmtext CustomDMText) ConverDriverValue(ctx context.Context, columnType *sql.ColumnType, tempDriverValue driver.Value, structFieldType *reflect.Type) (interface{}, error) { + // 如果需要使用structFieldType,需要先判断是否为nil + // if structFieldType != nil { + // } + + // 类型转换 + dmClob, isok := tempDriverValue.(*dm.DmClob) + if !isok { + return tempDriverValue, errors.New("->ConverDriverValue-->转换至*dm.DmClob类型失败") + } + if dmClob == nil || !dmClob.Valid { + return new(string), nil + } + // 获取长度 + dmlen, errLength := dmClob.GetLength() + if errLength != nil { + return dmClob, errLength + } + + // int64转成int类型 + strInt64 := strconv.FormatInt(dmlen, 10) + dmlenInt, errAtoi := strconv.Atoi(strInt64) + if errAtoi != nil { + return dmClob, errAtoi + } + + // 读取字符串 + str, errReadString := dmClob.ReadString(1, dmlenInt) + + // 处理空字符串或NULL造成的EOF错误 + if errReadString == io.EOF { + return new(string), nil + } + + return &str, errReadString +} +// RegisterCustomDriverValueConver 注册自定义的字段处理逻辑,用于驱动无法直接转换的场景,例如达梦的 TEXT 无法直接转化成 string +// 一般是放到init方法里进行注册 +func init() { + // dialectColumnType 值是 Dialect.字段类型 ,例如 dm.TEXT + zorm.RegisterCustomDriverValueConver("dm.TEXT", CustomDMText{}) +} + +**/ diff --git a/vendor/gitee.com/chunanyong/zorm/IEntity.go b/vendor/gitee.com/chunanyong/zorm/IEntity.go new file mode 100644 index 00000000..7e9699a4 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/IEntity.go @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +// IEntityStruct "struct"实体类的接口,所有的struct实体类都要实现这个接口 +// IEntityStruct The interface of the "struct" entity class, all struct entity classes must implement this interface +type IEntityStruct interface { + // 获取表名称 + // Get the table name. + GetTableName() string + + // 获取数据库表的主键字段名称.因为要兼容Map,只能是数据库的字段名称 + // Get the primary key field name of the database table. Because it is compatible with Map, it can only be the field name of the database + GetPKColumnName() string + + // GetPkSequence 主键序列 + // GetPkSequence Primary key sequence + GetPkSequence() string +} + +// IEntityMap 使用Map保存数据,用于不方便使用struct的场景,如果主键是自增或者序列,不要"entityMap.Set"主键的值 +// IEntityMap Use Map to save data for scenarios where it is not convenient to use struct +// If the primary key is auto-increment or sequence, do not "entity Map.Set" the value of the primary key +type IEntityMap interface { + // 获取表名称 + // Get the table name + GetTableName() string + + // 获取数据库表的主键字段名称.因为要兼容Map,只能是数据库的字段名称. + // Get the primary key field name of the database table. Because it is compatible with Map, it can only be the field name of the database. + GetPKColumnName() string + + // GetEntityMapPkSequence 主键序列,不能使用GetPkSequence方法名,避免默认实现了IEntityStruct接口 + // GetEntityMapPkSequence primary key sequence, you cannot use the GetPkSequence method name, to avoid the default implementation of IEntityStruct interface + GetEntityMapPkSequence() string + + // GetDBFieldMap 针对Map类型,记录数据库字段 + // GetDBFieldMap For Map type, record database fields. + GetDBFieldMap() map[string]interface{} + + // GetDBFieldMapKey 按照Set的先后顺序记录key值,也就是数据库字段,用于SQL排序 + // GetDBFieldMapKey records the key value, that is, the database field, in the order of the Set, which is used for SQL sorting + GetDBFieldMapKey() []string + // 设置数据库字段的值 + // Set the value of a database field. + Set(key string, value interface{}) map[string]interface{} +} + +// EntityStruct "IBaseEntity" 的基础实现,所有的实体类都匿名注入.这样就类似实现继承了,如果接口增加方法,调整这个默认实现即可 +// EntityStruct The basic implementation of "IBaseEntity", all entity classes are injected anonymously +// This is similar to implementation inheritance. If the interface adds methods, adjust the default implementation +type EntityStruct struct{} + +// 默认数据库的主键列名 +// Primary key column name of the default database +const defaultPkName = "id" + +//GetTableName 获取表名称,必须有具体的Struct实现,类似java的抽象方法,避免手误忘记写表名.如果有扩展需求,建议使用接口进行扩展,不要默认实现GetTableName +/* +func (entity *EntityStruct) GetTableName() string { + return "" +} +*/ + +// GetPKColumnName 获取数据库表的主键字段名称.因为要兼容Map,只能是数据库的字段名称 +// GetPKColumnName Get the primary key field name of the database table +// Because it is compatible with Map, it can only be the field name of the database +func (entity *EntityStruct) GetPKColumnName() string { + return defaultPkName +} + +// var defaultPkSequence = make(map[string]string, 0) + +// GetPkSequence 主键序列 +// GetPkSequence Primary key sequence +func (entity *EntityStruct) GetPkSequence() string { + return "" +} + +//-------------------------------------------------------------------------// + +// EntityMap IEntityMap的基础实现,可以直接使用或者匿名注入 +type EntityMap struct { + // 表名 + tableName string + // 主键列名 + PkColumnName string + // 主键序列,如果有值,优先级最高 + PkSequence string + // 数据库字段,不暴露外部 + dbFieldMap map[string]interface{} + // 列名,记录顺序 + dbFieldMapKey []string +} + +// NewEntityMap 初始化Map,必须传入表名称 +func NewEntityMap(tbName string) *EntityMap { + entityMap := EntityMap{} + entityMap.dbFieldMap = map[string]interface{}{} + entityMap.tableName = tbName + entityMap.PkColumnName = defaultPkName + entityMap.dbFieldMapKey = make([]string, 0) + return &entityMap +} + +// GetTableName 获取表名称 +func (entity *EntityMap) GetTableName() string { + return entity.tableName +} + +// GetPKColumnName 获取数据库表的主键字段名称.因为要兼容Map,只能是数据库的字段名称 +func (entity *EntityMap) GetPKColumnName() string { + return entity.PkColumnName +} + +// GetEntityMapPkSequence 主键序列,不能使用GetPkSequence方法名,避免默认实现了IEntityStruct接口 +// GetEntityMapPkSequence primary key sequence, you cannot use the GetPkSequence method name, to avoid the default implementation of IEntityStruct interface +func (entity *EntityMap) GetEntityMapPkSequence() string { + return entity.PkSequence +} + +// GetDBFieldMap 针对Map类型,记录数据库字段 +// GetDBFieldMap For Map type, record database fields +func (entity *EntityMap) GetDBFieldMap() map[string]interface{} { + return entity.dbFieldMap +} + +// GetDBFieldMapKey 按照Set的先后顺序记录key值,也就是数据库字段,用于SQL排序 +// GetDBFieldMapKey records the key value, that is, the database field, in the order of the Set, which is used for SQL sorting +func (entity *EntityMap) GetDBFieldMapKey() []string { + return entity.dbFieldMapKey +} + +// Set 设置数据库字段 +// Set Set database fields +func (entity *EntityMap) Set(key string, value interface{}) map[string]interface{} { + _, ok := entity.dbFieldMap[key] + if !ok { // 如果不存在 + entity.dbFieldMapKey = append(entity.dbFieldMapKey, key) + } + entity.dbFieldMap[key] = value + + return entity.dbFieldMap +} diff --git a/vendor/gitee.com/chunanyong/zorm/IGlobalTransaction.go b/vendor/gitee.com/chunanyong/zorm/IGlobalTransaction.go new file mode 100644 index 00000000..090e75cd --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/IGlobalTransaction.go @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import "context" + +// IGlobalTransaction 托管全局分布式事务接口 +type IGlobalTransaction interface { + // BeginGTX 开启全局分布式事务 + BeginGTX(ctx context.Context, globalRootContext context.Context) error + + // CommitGTX 提交全局分布式事务.不能命名为 Commit,不然就和gtx的Commit一致了,就递归调用自己了....... + CommitGTX(ctx context.Context, globalRootContext context.Context) error + + // RollbackGTX 回滚全局分布式事务 + RollbackGTX(ctx context.Context, globalRootContext context.Context) error + + // GetGTXID 获取全局分布式事务的XID + GetGTXID(ctx context.Context, globalRootContext context.Context) (string, error) + + // 重新包装为 seata/hptx 的context.RootContext + // context.RootContext 如果后续使用了 context.WithValue,类型就是context.valueCtx 就会造成无法再类型断言为 context.RootContext + // 所以DBDao里使用了 globalRootContext变量,区分业务的ctx和分布式事务的RootContext + // NewRootContext(ctx context.Context) context.Context +} diff --git a/vendor/gitee.com/chunanyong/zorm/LICENSE b/vendor/gitee.com/chunanyong/zorm/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/gitee.com/chunanyong/zorm/Logger.go b/vendor/gitee.com/chunanyong/zorm/Logger.go new file mode 100644 index 00000000..fe3a0033 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/Logger.go @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "context" + "fmt" + "log" +) + +func init() { + // 设置默认的日志显示信息,显示文件和行号 + // Set the default log display information, display file and line number. + log.SetFlags(log.Llongfile | log.LstdFlags) +} + +// LogCallDepth 记录日志调用层级,用于定位到业务层代码 +// Log Call Depth Record the log call level, used to locate the business layer code +var LogCallDepth = 4 + +// FuncLogError 记录error日志.NewDBDao方法里的异常,ctx为nil,扩展时请注意 +// FuncLogError Record error log +var FuncLogError func(ctx context.Context, err error) = defaultLogError + +// FuncLogPanic 记录panic日志,默认使用"defaultLogError"实现 +// FuncLogPanic Record panic log, using "defaultLogError" by default +var FuncLogPanic func(ctx context.Context, err error) = defaultLogPanic + +// FuncPrintSQL 打印sql语句,参数和执行时间,小于0是禁用日志输出;等于0是只输出日志,不计算SQ执行时间;大于0是计算执行时间,并且大于指定值 +// FuncPrintSQL Print sql statement and parameters +var FuncPrintSQL func(ctx context.Context, sqlstr string, args []interface{}, execSQLMillis int64) = defaultPrintSQL + +func defaultLogError(ctx context.Context, err error) { + log.Output(LogCallDepth, fmt.Sprintln(err)) +} + +func defaultLogPanic(ctx context.Context, err error) { + defaultLogError(ctx, err) +} + +func defaultPrintSQL(ctx context.Context, sqlstr string, args []interface{}, execSQLMillis int64) { + if args != nil { + log.Output(LogCallDepth, fmt.Sprintln("sql:", sqlstr, ",args:", args, ",execSQLMillis:", execSQLMillis)) + } else { + log.Output(LogCallDepth, fmt.Sprintln("sql:", sqlstr, ",args: [] ", ",execSQLMillis:", execSQLMillis)) + } +} diff --git a/vendor/gitee.com/chunanyong/zorm/Page.go b/vendor/gitee.com/chunanyong/zorm/Page.go new file mode 100644 index 00000000..ba6e42fa --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/Page.go @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +// Page 分页对象 +// Page Pagination object +type Page struct { + // 当前页码,从1开始 + // Current page number, starting from 1 + PageNo int + + // 每页多少条,默认20条 + // How many items per page, 20 items by default + PageSize int + + // 数据总条数 + // Total number of data + TotalCount int + + // 共多少页 + // How many pages + PageCount int + + // 是否是第一页 + // Is it the first page + FirstPage bool + + // 是否有上一页 + // Whether there is a previous page + HasPrev bool + + // 是否有下一页 + // Is there a next page + HasNext bool + + // 是否是最后一页 + // Is it the last page + LastPage bool +} + +// NewPage 创建Page对象 +// NewPage Create Page object +func NewPage() *Page { + page := Page{} + page.PageNo = 1 + page.PageSize = 20 + return &page +} + +// setTotalCount 设置总条数,计算其他值 +// setTotalCount Set the total number of bars, calculate other values +func (page *Page) setTotalCount(total int) { + page.TotalCount = total + page.PageCount = (page.TotalCount + page.PageSize - 1) / page.PageSize + if page.PageNo >= page.PageCount { + page.LastPage = true + } else { + page.HasNext = true + } + if page.PageNo > 1 { + page.HasPrev = true + } else { + page.FirstPage = true + } +} diff --git a/vendor/gitee.com/chunanyong/zorm/README.md b/vendor/gitee.com/chunanyong/zorm/README.md new file mode 100644 index 00000000..e94bae82 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/README.md @@ -0,0 +1,1094 @@ +## Introduction +![zorm logo](zorm-logo.png) +This is a lightweight ORM,zero dependency, that supports DM,Kingbase,shentong,TDengine,mysql,postgresql,oracle,mssql,sqlite,db2,clickhouse... + +Official website:https://zorm.cn +source code address:https://gitee.com/chunanyong/zorm +test case: https://gitee.com/wuxiangege/zorm-examples/ +Video tutorial: https://www.bilibili.com/video/BV1L24y1976U/ + + +``` +go get gitee.com/chunanyong/zorm +``` + +* Based on native SQL statements, the learning cost is lower +* [Code generator](https://gitee.com/zhou-a-xing/zorm-generate-struct) +* The code is concise, the main body is 2500 lines, zero dependency 4000 lines, detailed comments, easy to customize and modify +* Support for transaction propagation, which was the main reason for the birth of ZORM +* Support dm (dameng), kingbase (jincang), shentong (Shentong), gbase (Nantong), TDengine, mysql, postgresql, oracle, mssql, sqlite, db2, clickhouse... +* Supports multi-database and read/write splitting +* Joint primary keys are not supported, workarounds are assumed to be no primary keys, and business control is implemented (difficult trade-offs) +* Support seata, HPTX, dbpack distributed transactions, support global transaction hosting, do not modify business code, zero intrusion distributed transactions +* Support clickhouse, update, delete statements using SQL92 standard syntax.clickhouse-go official driver does not support batch insert syntax, it is recommended to use https://github.com/mailru/go-clickhouse + +## Transaction propagation +Transaction propagation is the core function of ZORM and the main reason why all methods of ZORM have ctx parameters. +ZORM transaction operations need ```zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {})``` to be explicitly enabled, check transactions before executing closure functions, join transactions if there are transactions in ctx, and create new transactions if there are no transactions in ctx, so you only need to pass the same ctx object to achieve transaction propagation. In special scenarios, if you do not want transaction synchronization, you can declare a new ctx object to do transaction isolation. + +## Description of the source repository +The main libraries of the open source projects I led are all in GitHub, and there are project descriptions on GitHub to guide the jump to GitHub, which also causes the slow growth of the project, after all, there are more GitHub users. +**Open source has no borders, but developers have their own homeland.** +Strictly speaking, GitHub is governed by US law https://www.infoq.cn/article/SA72SsSeZBpUSH_ZH8XB +do my best to support the domestic open source community, don't like it, don't spray, thank you! + +## Support domestic database +ZORM spares no effort in adapting to domestic databases, and if you encounter domestic databases that are not adapted or have problems, please feedback to the community and work together to build a domestic software ecosystem + +### Da Meng (DM) +- Configure zorm.DataSourceConfig ```DriverName:dm ,Dialect:dm``` +- Damon Database Driver: gitee.com/chunanyong/dm +- The TEXT type of Damon will be mapped to ```dm.DmClob```, string cannot be received, and zorm needs to be implemented ```ICustomDriverValueConver``` interface, custom extension processing +```go +import ( + // 00. Introduce the database driver + "gitee.com/chunanyong/dm" + "io" +) + +// CustomDMText implements ICustomDriverValueConver interface to extend custom types. For example, the TEXT type is mapped to dm.DmClob and cannot be directly received using string +type CustomDMText struct{} + +// GetDriverValue Returns an instance of driver.Value, the struct attribute type, based on the database column type +// The structFieldType is passed nil because the map received or field does not exist +func (dmtext CustomDMText) GetDriverValue(ctx context.Context, columnType *sql.ColumnType, structFieldType *reflect.Type) (driver.Value, error) { + // If you want to use the structFieldType, you need to determine if it is nil + // if structFieldType != nil { + // } + + return &dm.DmClob{}, nil +} + +// ConverDriverValue database column type, temporary received Value of driver. value returned by GetDriverValue,struct attribute type +// The structFieldType is passed nil because the map received or field does not exist +// Returns a pointer, pointer, pointer that matches the received type value!!!! +func (dmtext CustomDMText) ConverDriverValue(ctx context.Context, columnType *sql.ColumnType, tempDriverValue driver.Value, structFieldType *reflect.Type) (interface{}, error) { + // If you want to use the structFieldType, you need to determine if it is nil + // if structFieldType != nil { + // } + + // Type conversion + dmClob, isok := tempDriverValue.(*dm.DmClob) + if !isok { + return tempDriverValue, errors.New("->ConverDriverValue--> Failed to convert to *dm.DmClob") + } + if dmClob == nil || !dmClob.Valid { + return new(string), nil + } + // Get the length + dmlen, errLength := dmClob.GetLength() + if errLength != nil { + return dmClob, errLength + } + + // int64 is converted to an int + strInt64 := strconv.FormatInt(dmlen, 10) + dmlenInt, errAtoi := strconv.Atoi(strInt64) + if errAtoi != nil { + return dmClob, errAtoi + } + + // Read the string + str, errReadString := dmClob.ReadString(1, dmlenInt) + + // Handle EOF errors caused by empty strings or NULL value + if errReadString == io.EOF { + return new(string), nil + } + + return &str, errReadString +} +// RegisterCustomDriverValueConver registered custom field processing logic, used to drive not directly convert scenarios, such as the TEXT of the dream cannot directly into a string +// It's usually registered in the init method +func init() { + // dialectColumnType is a Dialect.FieldType, such as dm.TEXT + zorm.RegisterCustomDriverValueConver("dm.TEXT", CustomDMText{}) +} +``` +### Kingbase +- Configure zorm.DataSourceConfig ```DriverName:kingbase ,Dialect:kingbase``` +- Golden warehouse official drive: https://www.kingbase.com.cn/qd/index.htmhttps://bbs.kingbase.com.cn/thread-14457-1-1.html?_dsign=87f12756 +- The Kingbase 8 core is based on PostgreSQL 9.6 and can be tested using https://github.com/lib/pq, and the official driver is recommended for production environments +- Note that ora_input_emptystr_isnull = false or ora_input_emptystr_isnull = on in the data/kingbase.conf of the database (according to the version), because golang does not have a null value, the general database is not null, golang's string defaults to '', if this is set to true, The database will set the value to null, which conflicts with the field property not null, so an error is reported. + After the configuration file is modified, restart the database. +- Thanks to [@Jin](https://gitee.com/GOODJIN) for testing and suggestions. + +### Shentong (shentong) +It is recommended to use official driver, configure zorm.DataSourceConfig ```DriverName:aci ,Dialect:shentong``` + +### Nantong (gbase) +~~The official Go driver has not been found yet. Please configure it zorm.DataSourceConfig DriverName:gbase ,Dialect:gbase~~ +Use odbc driver for the time being, ```DriverName:odbc ,Dialect:gbase``` + +### TDengine +- Since the TDengine driver does not support transactions, you need to set this setting ```DisableTransaction=true``` +- Configure zorm.DataSourceConfig ```DriverName:taosSql/taosRestful, Dialect:tdengine``` +- zorm.DataSourceConfig```TDengineInsertsColumnName```TDengine batch insert statement whether there is a column name. The default false has no column name, and the insertion value and database column order are consistent, reducing the length of the statement +- Test case: https://www.yuque.com/u27016943/nrgi00/dnru3f +- TDengine is included: https://github.com/taosdata/awesome-tdengine/#orm + +## Database scripts and entity classes +Generate entity classes or write them manually, we recommend using a code generator https://gitee.com/zhou-a-xing/zorm-generate-struct + +```go + +package testzorm + +import ( + "time" + + "gitee.com/chunanyong/zorm" +) + +// Build a list sentence + +/* + +DROP TABLE IF EXISTS `t_demo`; +CREATE TABLE `t_demo` ( +`id` varchar(50) NOT NULL COMMENT 'primary key ', +`userName` varchar(30) NOT NULL COMMENT 'name ', +`password` varchar(50) NOT NULL COMMENT 'password ', +`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP(0), +`active` int COMMENT 'Whether it is valid (0 no,1 yes)', + PRIMARY KEY (`id`) +ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = 'example'; + +*/ + +// demoStructTableName Table name constant for direct call +const demoStructTableName = "t_demo" + +// demoStruct example +type demoStruct struct { + // Introduce the default struct to isolate method changes to the IEntityStruct + zorm.EntityStruct + + // Id Primary key + Id string `column:"id"` + + // UserName Specifies the name + UserName string `column:"userName"` + + // Password Password + Password string `column:"password"` + + // CreateTime + CreateTime time.Time `column:"createTime"` + + // Active Whether it is valid (0 No,1 yes) + // Active int `column:"active"` + + // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - end of the database field, custom fields to write in the following -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- // + // If the queried field is not found in the column tag, it is mapped to the struct property by name (case insensitive, support _ _ to _ hump) + + // Simulates the custom field Active + Active int +} + +// GetTableName Gets the table name +// IEntityStruct interface method, entity class needs to implement!! +func (entity *demoStruct) GetTableName() string { + return demoStructTableName +} + +// GetPKColumnName Gets the name of the primary key field of the database table. Because to be compatible with Map, can only be the database field name +// Joint primary key is not supported. It can be considered that there is no primary key and service control can be realized (hard choice). +// If you do not have a primary key, you need to implement this method as well +// IEntityStruct interface method, entity class needs to implement!! +func (entity *demoStruct) GetPKColumnName() string { + // If there is no primary key + // return "" + return "id" +} + +// newDemoStruct creates a default object +func newDemoStruct() demoStruct { + demo := demoStruct{ + // if Id == ", "save zorm will call zorm.FuncGenerateStringID(ctx), the default time stamp + random number, also can define your own implementations, such as zorm.FuncGenerateStringID = funcmyId + Id: zorm.FuncGenerateStringID(ctx), + UserName: "defaultUserName", + Password: "defaultPassword", + Active: 1, + CreateTime: time.Now(), + } + return demo +} +``` + +## Test cases are documents +https://gitee.com/wuxiangege/zorm-examples +```go + +// testzorm uses native sql statements with no restrictions on sql syntax. The statement uses Finder as the carrier +// Universal use of placeholders? zorm automatically replaces placeholders based on the database type, such as the postgresql database? Replace it with $1,$2... +// zorm uses the ctx context.Context parameter to propagate the transaction. ctx is passed in from the web layer. For example, gin's c.Request.Context() +// Transaction must be explicitly enabled using zorm.Transaction(ctx, func(ctx context.context) (interface{}, error) {}) +package testzorm + +import ( + "context" + "fmt" + "testing" + "time" + + "gitee.com/chunanyong/zorm" + + // 00. Introduce the database driver + _ "github.com/go-sql-driver/mysql" +) + +// DBDAOs represent one database. If there are multiple databases, multiple DBDAOs are declared +var dbDao *zorm.DBDao + +// 01. Initialize the DBDao +func init() { + + // Customize zorm log output + // zorm.LogCallDepth = 4 // Level of log calls + // zorm.FuncLogError = myFuncLogError // Function to log exceptions + // zorm.FuncLogPanic = myFuncLogPanic // To log panic, the default is defaultLogError + // zorm.FuncPrintSQL = myFuncPrintSQL // A function that prints sql + + // Reassign the FuncPrintSQL function to a custom log output format + // log.SetFlags(log.LstdFlags) + // zorm.FuncPrintSQL = zorm.FuncPrintSQL + + // Custom primary key generation + // zorm.FuncGenerateStringID=funcmyId + + // Customize the Tag column name + // zorm.FuncWrapFieldTagName=funcmyTagName + + // Custom decimal type implementation + // zorm.FuncDecimalValue=funcmyDecimal + + // the Go database driver list: https://github.com/golang/go/wiki/SQLDrivers + + // dbDaoConfig Configure the database. This is just a simulation, the production should be reading the configuration configuration file and constructing the DataSourceConfig + dbDaoConfig := zorm.DataSourceConfig{ + // DSN database connection string. parseTime=true is automatically converted to time format. The default query is the []byte array + DSN: "root:root@tcp(127.0.0.1:3306)/zorm?charset=utf8&parseTime=true", + // DriverName database driver name: mysql, postgres, oracle(go-ora), essentially, sqlite3, go_ibm_db, clickhouse, dm, kingbase, aci, taosSql | taosRestful Correspond to Dialect + // sql.Open(DriverName,DSN) DriverName is the first string parameter of the sql.Open of the driver. The value can be obtained according to the actual conditions of the driver + DriverName: "mysql", + // the Dialect database Dialect: mysql, postgresql, oracle, MSSQL, sqlite, db2, clickhouse, dm, kingbase, shentong, tdengine and DriverName corresponding + Dialect: "mysql", + // MaxOpenConns The default maximum number of database connections is 50 + MaxOpenConns: 50, + // MaxIdleConns The default maximum number of idle connections is 50 + MaxIdleConns: 50, + // ConnMaxLifetimeSecond Connection survival seconds. Default 600(10 minutes) after the connection is destroyed and rebuilt. Prevent the database from voluntarily disconnecting, resulting in dead connections. MySQL default wait_timeout 28800 seconds (8 hours) + ConnMaxLifetimeSecond: 600, + // SlowSQLMillis slow sql time threshold, in milliseconds. A value less than 0 disables SQL statement output. If the value is equal to 0, only SQL statements are output and the execution time is not calculated. A value greater than 0 is used to calculate the SQL execution time and >=SlowSQLMillis value + SlowSQLMillis: 0, + // DefaultTxOptions Default configuration of transaction isolation level, which defaults to nil + // DefaultTxOptions: nil, + // If distributed transactions are used, the default configuration is recommended + // DefaultTxOptions: &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}, + + // FuncGlobalTransaction seata/hptx An adaptation function of a globally distributed transaction that returns the implementation of the IGlobalTransaction interface + // business must call ctx, _ = zorm.BindContextEnableGlobalTransaction (ctx) on the global distribution of transactions + // FuncGlobalTransaction : MyFuncGlobalTransaction, + + // SQLDB uses an existing database connection and has a higher priority than DSN + // SQLDB : nil, + + // DisableTransaction disables transactions. The default value is false. If DisableTransaction=true is set, the Transaction method becomes invalid and no transaction is required. Some databases, such as TDengine, do not support transactions + // Disable transactions should have the driver forgery transaction API, there should be no orm implementation,clickhouse's driver does just that + // DisableTransaction :false, + + // TDengineInsertsColumnName Whether there are column names in the TDengine batch insert statement. The default false has no column name, and the insertion value and database column order are consistent, reducing the length of the statement + // TDengineInsertsColumnName :false, + } + + // Create dbDao based on dbDaoConfig. Perform this operation once for each database. The first database is defaultDao and the subsequent zorm.xxx method uses defaultDao by default + dbDao, _ = zorm.NewDBDao(&dbDaoConfig) +} + +// TestInsert 02. Test save the Struct object +func TestInsert(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // Create a demo object + demo := newDemoStruct() + + // Save the object. The parameter is a pointer to the object. If the primary key is increment, the value is assigned to the primary key property of the object + _, err := zorm.Insert(ctx, &demo) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + // Mark the test failed + if err != nil { + t.Errorf("Error:%v", err) + } +} + +// TestInsertSlice 03. Tests batch save Struct object Slice +// The primary key property in the Struct object cannot be assigned if the primary key is autoincrement +func TestInsertSlice(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // slice stores the type zorm.IEntityStruct!!! Use the IEntityStruct interface, compatible with Struct entity classes + demoSlice := make([]zorm.IEntityStruct,0) + + // Create object 1 + demo1 := newDemoStruct() + demo1.UserName = "demo1" + // Create object 2 + demo2 := newDemoStruct() + demo2.UserName = "demo2" + + demoSlice = append(demoSlice, &demo1, &demo2) + + // Batch save objects. If the primary key is auto-increment, the auto-increment ID cannot be saved to the object. + _, err := zorm.InsertSlice(ctx, demoSlice) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + // Mark the test failed + if err != nil { + t.Errorf("Error:%v", err) + } +} + +// TestInsertEntityMap 04. Test to save an EntityMap object for scenarios where it is not convenient to use struct. Use Map as the carrier +func TestInsertEntityMap(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // To create an EntityMap, pass in the table name + entityMap := zorm.NewEntityMap(demoStructTableName) + // Set the primary key name + entityMap.PkColumnName = "id" + // If it is an increment sequence, set the value of the sequence + // entityMap.PkSequence = "mySequence" + + // Set Sets the field values of the database + // If the primary key is an increment or sequence, do not set the value of the entityMap.Set primary key + entityMap.Set("id", zorm.FuncGenerateStringID(ctx)) + entityMap.Set("userName", "entityMap-userName") + entityMap.Set("password", "entityMap-password") + entityMap.Set("createTime", time.Now()) + entityMap.Set("active", 1) + + // Execute + _, err := zorm.InsertEntityMap(ctx, entityMap) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + // Mark the test failed + if err != nil { + t.Errorf("Error:%v", err) + } +} + + +// TestInsertEntityMapSlice 05. Test batch save []IEntityMap for scenarios where it is not convenient to use struct, using Map as carrier +func TestInsertEntityMapSlice(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + _, err := Transaction(ctx, func(ctx context.Context) (interface{}, error) { + entityMapSlice := make([]IEntityMap, 0) + entityMap1 := NewEntityMap(demoStructTableName) + entityMap1.PkColumnName = "id" + entityMap1.Set("id", zorm.FuncGenerateStringID(ctx)) + entityMap1.Set("userName", "entityMap-userName1") + entityMap1.Set("password", "entityMap-password1") + entityMap1.Set("createTime", time.Now()) + entityMap1.Set("active", 1) + + entityMap2 := NewEntityMap(demoStructTableName) + entityMap2.PkColumnName = "id" + entityMap2.Set("id", zorm.FuncGenerateStringID(ctx)) + entityMap2.Set("userName", "entityMap-userName2") + entityMap2.Set("password", "entityMap-password2") + entityMap2.Set("createTime", time.Now()) + entityMap2.Set("active", 2) + + entityMapSlice = append(entityMapSlice, entityMap1 ,entityMap2) + + // Execute + _, err := zorm.InsertEntityMapSlice(ctx, entityMapSlice) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + // Mark the test failed + if err != nil { + t.Errorf("Error:%v", err) + } +} + +// TestQueryRow 06. Test query a struct object +func TestQueryRow(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // Declares a pointer to an object that holds the returned data + demo := demoStruct{} + + // finder used to construct the query + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + // finder := zorm.NewSelectFinder(demoStructTableName, "id,userName") // select id,userName from t_demo + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // finder by default, sql injection checking is enabled to disallow concatenation of 'single quotes in statements. You can set finder.injectioncheck = false to undo the restriction + + // finder.Append The first argument is the statement and the following arguments are the corresponding values in the correct order. Uniform use of statements? zorm handles database differences + // in (?) Arguments must have () parentheses, not in? + finder.Append("WHERE id=? and active in(?) ", "20210630163227149563000042432429", []int{0, 1}) + + // How do I use like + // finder.Append("WHERE id like ? ", "20210630163227149563000042432429%") + + // If the value of "has" is true, the database has data + has, err := zorm.QueryRow(ctx, finder, &demo) + + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + // Print the result + fmt.Println(demo) +} + +// TestQueryRowMap 07. Test query map receives results. It is flexible for scenarios that are not suitable for structs +func TestQueryRowMap(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // finder used to construct the query + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // finder.Append The first argument is the statement and the following arguments are the corresponding values in the correct order. Uniform use of statements? zorm handles database differences + // in (?) Arguments must have () parentheses, not in? + finder.Append("WHERE id=? and active in(?) ", "20210630163227149563000042432429", []int{0, 1}) + // Run the query + resultMap, err := zorm.QueryRowMap(ctx, finder) + + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + // Print the result + fmt.Println(resultMap) +} + +// TestQuery 08. Test the list of query objects +func TestQuery(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // Create a slice for receiving results + list := make([]demoStruct, 0) + + // finder used to construct the query + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + finder := zorm.NewFinder().Append("SELECT id FROM " + demoStructTableName) // select * from t_demo + // Create a paging object. After the query is complete, the page object can be directly used by the front-end paging component + page := zorm.NewPage() + page.PageNo = 1 // Query page 1. The default value is 1 + page.PageSize = 20 // 20 per page. The default is 20 + + // The total number of entries is not queried + // finder.SelectTotalCount = false + + // You can manually specify paging statements if they are particularly complex statements that cause count statement construction to fail + // countFinder := zorm.NewFinder().Append("select count(*) from (") + // countFinder.AppendFinder(finder) + // countFinder.Append(") tempcountfinder") + // finder.CountFinder = countFinder + + // Run the query + err := zorm.Query(ctx, finder, &list, page) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + // Print the result + fmt.Println("Total number of items :", page.TotalCount, "List :", list) +} + +// TestQueryMap 09. Test query map list. Used in the scenario where struct is not convenient +func TestQueryMap(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // finder used to construct the query + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // Create a paging object. After the query is complete, the page object can be directly used by the front-end paging component + page := zorm.NewPage() + page.PageNo = 1 // Query page 1. The default value is 1 + page.PageSize = 20 // 20 per page. The default is 20 + + // The total number of entries is not queried + // finder.SelectTotalCount = false + + // You can manually specify paging statements if they are particularly complex statements that cause count statement construction to fail + // countFinder := zorm.NewFinder().Append("select count(*) from (") + // countFinder.AppendFinder(finder) + // countFinder.Append(") tempcountfinder") + // finder.CountFinder = countFinder + + // Run the query + listMap, err := zorm.QueryMap(ctx, finder, page) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + // Print the result + fmt.Println("Total number of items :", page.TotalCount, "List :", listMap) +} + +// TestUpdateNotZeroValue 10. Update the struct object with only the non-zero fields. The primary key must have a value +func TestUpdateNotZeroValue(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // Declares a pointer to an object used to update data + demo := demoStruct{} + demo.Id = "20210630163227149563000042432429" + demo.UserName = "UpdateNotZeroValue" + + // UPDATE "sql":"UPDATE t_demo SET userName=? WHERE id=?" ,"args":["UpdateNotZeroValue","20210630163227149563000042432429"] + _, err := zorm.UpdateNotZeroValue(ctx, &demo) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + +} + +// TestUpdate 11. Update the struct object, updating all fields. The primary key must have a value +func TestUpdate(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // Declares a pointer to an object used to update data + demo := demoStruct{} + demo.Id = "20210630163227149563000042432429" + demo.UserName = "TestUpdate" + + _, err := zorm.Update(ctx, &demo) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } +} + +// TestUpdateFinder 12. With finder update,zorm's most flexible way of writing any update statement, even manually writing insert statements +func TestUpdateFinder(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // finder := zorm.NewUpdateFinder(demoStructTableName) // UPDATE t_demo SET + // finder := zorm.NewDeleteFinder(demoStructTableName) // DELETE FROM t_demo + finder := zorm.NewFinder().Append("UPDATE").Append(demoStructTableName).Append("SET") // UPDATE t_demo SET + finder.Append("userName=? ,active=?", "TestUpdateFinder", 1).Append("WHERE id=?", "20210630163227149563000042432429") + + // UPDATE "sql":"UPDATE t_demo SET userName=? ,active=? WHERE id=?" ,"args":["TestUpdateFinder",1,"20210630163227149563000042432429"] + _, err := zorm.UpdateFinder(ctx, finder) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + +} + +// TestUpdateEntityMap 13. Update an EntityMap. The primary key must have a value +func TestUpdateEntityMap(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // To create an EntityMap, pass in the table name + entityMap := zorm.NewEntityMap(demoStructTableName) + // Set the primary key name + entityMap.PkColumnName = "id" + // Set Sets the field value of the database. The primary key must have a value + entityMap.Set("id", "20210630163227149563000042432429") + entityMap.Set("userName", "TestUpdateEntityMap") + // UPDATE "sql":"UPDATE t_demo SET userName=? WHERE id=?" ,"args":["TestUpdateEntityMap","20210630163227149563000042432429"] + _, err := zorm.UpdateEntityMap(ctx, entityMap) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + +} + +// TestDelete 14. Delete a struct object. The primary key must have a value +func TestDelete(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // You need to start the transaction manually. If the error returned by the anonymous function is not nil, the transaction will be rolled back. If the DisableTransaction=true parameter is set, the Transaction method becomes invalid and no transaction is required + // if zorm.DataSourceConfig.DefaultTxOptions configuration does not meet the requirements, can be in zorm, Transaction before Transaction method set the Transaction isolation level + // such as ctx, _ := dbDao BindContextTxOptions (ctx, & SQL TxOptions {Isolation: SQL LevelDefault, ReadOnly: False}), if txOptions is nil, the use of zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + demo := demoStruct{} + demo.Id = "20210630163227149563000042432429" + + // "sql":"DELETE FROM t_demo WHERE id=?" ,"args":["20210630163227149563000042432429"] + _, err := zorm.Delete(ctx, &demo) + + // If err is not returned nil, the transaction is rolled back + return nil, err + }) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + +} + +// TestProc 15. Test calls the stored procedure +func TestProc(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + demo := demoStruct{} + finder := zorm.NewFinder().Append("call testproc(?)", "u_10001") + zorm.QueryRow(ctx, finder, &demo) + fmt.Println(demo) +} + +// TestFunc 16. Test calls custom functions +func TestFunc(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + userName := "" + finder := zorm.NewFinder().Append("select testfunc(?)", "u_10001") + zorm.QueryRow(ctx, finder, &userName) + fmt.Println(userName) +} + +// TestOther 17. Some other instructions. Thank you very much for seeing this line +func TestOther(t *testing.T) { + // ctx is generally a request for one ctx, normally there should be a web layer in, such as gin's c. Request.Context() + var ctx = context.Background() + + // Scenario 1. Multiple databases. The dbDao of the corresponding database calls BindContextDBConnection, binds the database connection to the returned ctx, and passes ctx to zorm's function + // You can also rewrite the FuncReadWriteStrategy function to return the DBDao of the specified database by setting a different key via ctx + newCtx, err := dbDao.BindContextDBConnection(ctx) + if err != nil { // Mark the test failed + t.Errorf("Error:%v", err) + } + + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // Pass the new newCtx to zorm's function + list, _ := zorm.QueryMap(newCtx, finder, nil) + fmt.Println(list) + + // Scenario 2. Read/write separation of a single database. Set the read-write separation policy function. + zorm.FuncReadWriteStrategy = myReadWriteStrategy + + // Scenario 3. If multiple databases exist and read and write data are separated from each other, perform this operation according to Scenario 1. + // You can also rewrite the FuncReadWriteStrategy function to return the DBDao of the specified database by setting a different key via ctx + +} + +// myReadWriteStrategy Database read-write strategy rwType=0 read,rwType=1 write +// You can also set different keys through ctx to return the DBDao of the specified database +func myReadWriteStrategy(ctx context.Context, rwType int) (*zorm.DBDao, error) { + // Return the required read/write dao based on your business scenario. This function is called every time a database connection is needed + // if rwType == 0 { + // return dbReadDao + // } + // return dbWriteDao + + return dbDao, nil +} + +// -------------------------------------------- +// ICustomDriverValueConver interface, see examples of DaMeng + +// -------------------------------------------- +// OverrideFunc Rewrite the functions of ZORM, when you use this function, you have to know what you are doing + +``` +## Global transaction +### seata-go CallbackWithCtx function mode +```go +// DataSourceConfig configures DefaultTxOptions +// DefaultTxOptions: &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}, + +// Import the seata-go dependency package +import ( + "context" + "fmt" + "time" + + "github.com/seata/seata-go/pkg/client" + "github.com/seata/seata-go/pkg/tm" + seataSQL "github.com/seata/seata-go/pkg/datasource/sql" //Note: zorm's DriverName: seataSQL.SeataATMySQLDriver, !!!! +) + +// Path of the configuration file +var configPath = "./conf/client.yml" + +func main() { + + // Initialize the configuration + conf := config.InitConf(configPath) + // Initialize the zorm database + // note: zorm DriverName: seataSQL SeataATMySQLDriver,!!!!!!!!!! + initZorm() + + // Start distributed transactions + tm.WithGlobalTx(context.Background(), &tm.GtxConfig{ + Name: "ATSampleLocalGlobalTx", + Timeout: time.Second * 30, + }, CallbackWithCtx) + // CallbackWithCtx business callback definition + // type CallbackWithCtx func(ctx context.Context) error + + + // Get the XID after the transaction is started. This can be passed through gin's header, or otherwise + // xid:=tm.GetXID(ctx) + // tm.SetXID(ctx, xid) + + // If the gin framework is used, middleware binding parameters can be used + // r.Use(ginmiddleware.TransactionMiddleware()) +} + +``` + +### seata-go transaction hosting mode + +```go +// Do not use CallbackWithCtx function,zorm to achieve transaction management, no modification of business code, zero intrusion to achieve distributed transactions + + +// The distributed transaction must be started manually and must be invoked before the local transaction is started +ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) +// Distributed transaction sample code +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // Get the XID of the current distributed transaction. Don't worry about how, if it is a distributed transaction environment, the value will be set automatically + // xid := ctx.Value("XID").(string) + + // Pass the xid to the third party application + // req.Header.Set("XID", xid) + + // If err is not returned nil, local and distributed transactions are rolled back + return nil, err +}) + +// /---------- Third-party application -------/ // + + // Do not use the middleware provided by seata-go by default, just ctx binding XID!!! + //// r.Use(ginmiddleware.TransactionMiddleware()) + xid := c.GetHeader(constant.XidKey) + ctx = context.WithValue(ctx, "XID", xid) + + // The distributed transaction must be started manually and must be invoked before the local transaction is started + ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) + // ctx invokes the business transaction after binding the XID + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // Business code...... + + // If err is not returned nil, local and distributed transactions are rolled back + return nil, err +}) + +// It is recommended that the following code be placed in a separate file +// ... // + +// ZormGlobalTransaction packaging seata *tm.GlobalTransactionManager, zorm.IGlobalTransaction interface +type ZormGlobalTransaction struct { + *tm.GlobalTransactionManager +} + +// MyFuncGlobalTransaction zorm A function that ADAPTS a seata globally distributed transaction +// important!!!! Need to configure the zorm.DataSourceConfig.FuncGlobalTransaction = MyFuncGlobalTransaction important!!!!!! +func MyFuncGlobalTransaction(ctx context.Context) (zorm.IGlobalTransaction, context.Context, context.Context, error) { + // Create a seata-go transaction + globalTx := tm.GetGlobalTransactionManager() + // Use the zorm.IGlobalTransaction interface object to wrap distributed transactions and isolate the seata-go dependencies + globalTransaction := &ZormGlobalTransaction{globalTx} + + if tm.IsSeataContext(ctx) { + return globalTransaction, ctx, ctx, nil + } + // open global transaction for the first time + ctx = tm.InitSeataContext(ctx) + // There is a request to come in, manually get the XID + xidObj := ctx.Value("XID") + if xidObj ! = nil { + xid := xidObj.(string) + tm.SetXID(ctx, xid) + } + tm.SetTxName(ctx, "ATSampleLocalGlobalTx") + + // use new context to process current global transaction. + if tm.IsGlobalTx(ctx) { + globalRootContext := transferTx(ctx) + return globalTransaction, ctx, globalRootContext, nil + } + return globalTransaction, ctx, ctx, nil +} + +// IGlobalTransaction managed global distributed transaction interface (zorm.IGlobalTransaction). seata and hptx currently implement the same code, only the reference implementation package is different + +// BeginGTX Starts global distributed transactions +func (gtx *ZormGlobalTransaction) BeginGTX(ctx context.Context, globalRootContext context.Context) error { + //tm.SetTxStatus(globalRootContext, message.GlobalStatusBegin) + err := gtx.Begin(globalRootContext, time.Second*30) + return err +} + +// CommitGTX Commit global distributed transactions +func (gtx *ZormGlobalTransaction) CommitGTX(ctx context.Context, globalRootContext context.Context) error { + gtr := tm.GetTx(globalRootContext) + return gtx.Commit(globalRootContext, gtr) +} + +// RollbackGTX rolls back globally distributed transactions +func (gtx *ZormGlobalTransaction) RollbackGTX(ctx context.Context, globalRootContext context.Context) error { + gtr := tm.GetTx(globalRootContext) + // If it is the Participant role, change it to the Launcher role to allow branch transactions to submit global transactions. + if gtr.TxRole != tm.Launcher { + gtr.TxRole = tm.Launcher + } + return gtx.Rollback(globalRootContext, gtr) +} +// GetGTXID Gets the XID of the globally distributed transaction +func (gtx *ZormGlobalTransaction) GetGTXID(ctx context.Context, globalRootContext context.Context) (string.error) { + return tm.GetXID(globalRootContext), nil +} + +// transferTx transfer the gtx into a new ctx from old ctx. +// use it to implement suspend and resume instead of seata java +func transferTx(ctx context.Context) context.Context { + newCtx := tm.InitSeataContext(context.Background()) + tm.SetXID(newCtx, tm.GetXID(ctx)) + return newCtx +} + +// ... // +``` + + +### hptx proxy mode +[in hptx proxy mode for zorm use example](https://github.com/CECTC/hptx-samples/tree/main/http_proxy_zorm) +```go +// DataSourceConfig configures DefaultTxOptions +// DefaultTxOptions: &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}, + +// Introduce the hptx dependency package +import ( + "github.com/cectc/hptx" + "github.com/cectc/hptx/pkg/config" + "github.com/cectc/hptx/pkg/resource" + "github.com/cectc/mysql" + "github.com/cectc/hptx/pkg/tm" + + gtxContext "github.com/cectc/hptx/pkg/base/context" +) + +// Path of the configuration file +var configPath = "./conf/config.yml" + +func main() { + + // Initialize the configuration + hptx.InitFromFile(configPath) + + // Register the mysql driver + mysql.RegisterResource(config.GetATConfig().DSN) + resource.InitATBranchResource(mysql.GetDataSourceManager()) + // sqlDB, err := sql.Open("mysql", config.GetATConfig().DSN) + + + // After the normal initialization of zorm, be sure to put it after the hptx mysql initialization!! + + // ... // + // tm register transaction service, refer to the official example (transaction hosting is mainly to remove proxy, zero intrusion on the business) + tm.Implement(svc.ProxySvc) + // ... // + + + // Get the hptx rootContext + // rootContext := gtxContext.NewRootContext(ctx) + // rootContext := ctx.(*gtxContext.RootContext) + + // Create an hptx transaction + // globalTx := tm.GetCurrentOrCreate(rootContext) + + // Start the transaction + // globalTx. BeginWithTimeoutAndName (int32 (6000), "name of the transaction," rootContext) + + // Get the XID after the transaction is started. This can be passed through the gin header, or otherwise + // xid:=rootContext.GetXID() + + // If using gin frame, get ctx + // ctx := c.Request.Context() + + // Accept the XID passed and bind it to the local ctx + // ctx =context.WithValue(ctx,mysql.XID,xid) +} +``` + +### hptx transaction hosting mode +[zorm transaction hosting hptx example](https://github.com/CECTC/hptx-samples/tree/main/http_zorm) +```go +// Do not use proxy proxy mode,zorm to achieve transaction management, no modification of business code, zero intrusion to achieve distributed transactions +// tm.Implement(svc.ProxySvc) + +// The distributed transaction must be started manually and must be invoked before the local transaction is started +ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) +// Distributed transaction sample code +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // Get the XID of the current distributed transaction. Don't worry about how, if it is a distributed transaction environment, the value will be set automatically + // xid := ctx.Value("XID").(string) + + // Pass the xid to the third party application + // req.Header.Set("XID", xid) + + // If err is not returned nil, local and distributed transactions are rolled back + return nil, err +}) + +// /---------- Third-party application -------// / + +// Before third-party applications can start transactions,ctx needs to bind Xids, such as gin framework + +// Accept the XID passed and bind it to the local ctx +// xid:=c.Request.Header.Get("XID") +// ctx is obtained +// ctx := c.Request.Context() +// ctx = context.WithValue(ctx,"XID",xid) + +// The distributed transaction must be started manually and must be invoked before the local transaction is started +ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) +// ctx invokes the business transaction after binding the XID +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // Business code...... + + // If err is not returned nil, local and distributed transactions are rolled back + return nil, err +}) + + + +// It is recommended that the following code be placed in a separate file +// ... // + +// ZormGlobalTransaction packaging hptx *tm.DefaultGlobalTransaction, zorm.IGlobalTransaction interface +type ZormGlobalTransaction struct { + *tm.DefaultGlobalTransaction +} + +// MyFuncGlobalTransaction zorm A function that ADAPTS a hptx globally distributed transaction +// important!!!! Need to configure the zorm.DataSourceConfig.FuncGlobalTransaction = MyFuncGlobalTransaction important!!!!!! +func MyFuncGlobalTransaction(ctx context.Context) (zorm.IGlobalTransaction, context.Context, context.Context, error) { + // Obtain the hptx rootContext + rootContext := gtxContext.NewRootContext(ctx) + // Create a hptx transaction + globalTx := tm.GetCurrentOrCreate(rootContext) + // Use the zorm.IGlobalTransaction interface object to wrap distributed transactions and isolate hptx dependencies + globalTransaction := &ZormGlobalTransaction{globalTx} + + return globalTransaction, ctx, rootContext, nil +} + +// IGlobalTransaction managed global distributed transaction interface (zorm.IGlobalTransaction). seata and hptx currently implement the same code, only the reference implementation package is different + +// BeginGTX Starts global distributed transactions +func (gtx *ZormGlobalTransaction) BeginGTX(ctx context.Context, globalRootContext context.Context) error { + rootContext := globalRootContext.(*gtxContext.RootContext) + return gtx.BeginWithTimeout(int32(6000), rootContext) +} + +// CommitGTX Commit global distributed transactions +func (gtx *ZormGlobalTransaction) CommitGTX(ctx context.Context, globalRootContext context.Context) error { + rootContext := globalRootContext.(*gtxContext.RootContext) + return gtx.Commit(rootContext) +} + +// RollbackGTX rolls back globally distributed transactions +func (gtx *ZormGlobalTransaction) RollbackGTX(ctx context.Context, globalRootContext context.Context) error { + rootContext := globalRootContext.(*gtxContext.RootContext) + // If it is the Participant role, change it to the Launcher role to allow branch transactions to submit global transactions. + if gtx.Role != tm.Launcher { + gtx.Role = tm.Launcher + } + return gtx.Rollback(rootContext) +} +// GetGTXID Gets the XID of the globally distributed transaction +func (gtx *ZormGlobalTransaction) GetGTXID(ctx context.Context, globalRootContext context.Context) (string.error) { + rootContext := globalRootContext.(*gtxContext.RootContext) + return rootContext.GetXID(), nil +} + +// ... // +``` +### dbpack distributed transactions +```dbpack``` document: https://cectc.github.io/dbpack-doc/#/README deployment with a Mesh, the application integration is simple, just need to get xid, in a hint of SQL statements +```go +// Before starting dbpack transactions,ctx needs to bind sql hints, such as using the gin framework to obtain the xid passed by the header +xid := c.Request.Header.Get("xid") +// Generate sql hint content using xid, and then bind the hint to ctx +hint := fmt.Sprintf("/*+ XID('%s') */", xid) +// ctx is obtained +ctx := c.Request.Context() +// Bind the hint to ctx +ctx,_ = zorm.BindContextSQLHint(ctx, hint) + +// After ctx binds the sql hint, the business transaction is invoked and ctx is transmitted to realize the propagation of the distributed transaction +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // Business code...... + + // If err is not returned nil, local and distributed transactions are rolled back + return nil, err +}) +``` diff --git a/vendor/gitee.com/chunanyong/zorm/README_zh.md b/vendor/gitee.com/chunanyong/zorm/README_zh.md new file mode 100644 index 00000000..d3e36d96 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/README_zh.md @@ -0,0 +1,1111 @@ +## 介绍 +![zorm logo](zorm-logo.png) +Go轻量ORM,零依赖,零侵入分布式事务,支持达梦(dm),金仓(kingbase),神通(shentong),南通(gbase),TDengine,mysql,postgresql,oracle,mssql,sqlite,db2,clickhouse... + +官网: https://zorm.cn +源码地址: https://gitee.com/chunanyong/zorm +测试用例: https://gitee.com/wuxiangege/zorm-examples/ +视频教程: https://www.bilibili.com/video/BV1L24y1976U/ + +交流QQ群:[727723736]() 添加进入社区群聊,问题交流,技术探讨 +社区微信: [LAUV927]() + +``` +go get gitee.com/chunanyong/zorm +``` +* 基于原生sql语句,学习成本更低 +* [代码生成器](https://gitee.com/zhou-a-xing/zorm-generate-struct) +* 代码精简,主体2500行,零依赖4000行,注释详细,方便定制修改 +* 支持事务传播,这是zorm诞生的主要原因 +* 支持dm(达梦),kingbase(金仓),shentong(神通),gbase(南通),TDengine,mysql,postgresql,oracle,mssql,sqlite,db2,clickhouse... +* 支持多库和读写分离 +* 不支持联合主键,变通认为无主键,业务控制实现(艰难取舍) +* 支持seata,hptx,dbpack分布式事务,支持全局事务托管,不修改业务代码,零侵入分布式事务 +* 支持clickhouse,更新,删除语句使用SQL92标准语法.clickhouse-go官方驱动不支持批量insert语法,建议使用https://github.com/mailru/go-clickhouse + +## 事务传播 +事务传播是zorm的核心功能,也是zorm所有方法都有ctx入参的主要原因. +zorm的事务操作需要显式使用```zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {})```开启,在执行闭包函数前检查事务,如果ctx里有事务就加入事务,如果ctx里没事务就创建新的事务,所以只需要传递同一个ctx对象,就可以实现事务传播.特殊场景如果不想事务同步,就可以声明一个新的ctx对象,做事务隔离. + +## 源码仓库说明 +我主导的开源项目主库都在gitee,github上留有项目说明,引导跳转到gitee,这样也造成了项目star增长缓慢,毕竟github用户多些. +**开源没有国界,开发者却有自己的祖国.** +严格意义上,github是受美国法律管辖的 https://www.infoq.cn/article/SA72SsSeZBpUSH_ZH8XB +尽我所能,支持国内开源社区,不喜勿喷,谢谢! + +## 支持国产数据库 +zorm对国产数据库的适配不遗余力,遇到没有适配或者有问题的国产数据库,请反馈到社区,携手共建国产软件生态. +### 达梦(dm) +- 配置zorm.DataSourceConfig的 ```DriverName:dm ,Dialect:dm``` +- 达梦数据库驱动: gitee.com/chunanyong/dm +- 达梦的TEXT类型会映射为dm.DmClob,string不能接收,需要实现zorm.ICustomDriverValueConver接口,自定义扩展处理 +- 达梦开启等保参数 COMM_ENCRYPT_NAME = AES128_ECB , 会导致驱动连接异常 +```go +import ( + // 00.引入数据库驱动 + "gitee.com/chunanyong/dm" + "io" +) + +// CustomDMText 实现ICustomDriverValueConver接口,扩展自定义类型,例如 达梦数据库TEXT类型,映射出来的是dm.DmClob类型,无法使用string类型直接接收 +type CustomDMText struct{} + +// GetDriverValue 根据数据库列类型,返回driver.Value的实例,struct属性类型 +// map接收或者字段不存在,无法获取到structFieldType,会传入nil +func (dmtext CustomDMText) GetDriverValue(ctx context.Context, columnType *sql.ColumnType, structFieldType *reflect.Type) (driver.Value, error) { + // 如果需要使用structFieldType,需要先判断是否为nil + // if structFieldType != nil { + // } + + return &dm.DmClob{}, nil +} + +// ConverDriverValue 数据库列类型,GetDriverValue返回的driver.Value的临时接收值,struct属性类型 +// map接收或者字段不存在,无法获取到structFieldType,会传入nil +// 返回符合接收类型值的指针,指针,指针!!!! +func (dmtext CustomDMText) ConverDriverValue(ctx context.Context, columnType *sql.ColumnType, tempDriverValue driver.Value, structFieldType *reflect.Type) (interface{}, error) { + // 如果需要使用structFieldType,需要先判断是否为nil + // if structFieldType != nil { + // } + + // 类型转换 + dmClob, isok := tempDriverValue.(*dm.DmClob) + if !isok { + return tempDriverValue, errors.New("->ConverDriverValue-->转换至*dm.DmClob类型失败") + } + if dmClob == nil || !dmClob.Valid { + return new(string), nil + } + // 获取长度 + dmlen, errLength := dmClob.GetLength() + if errLength != nil { + return dmClob, errLength + } + + // int64转成int类型 + strInt64 := strconv.FormatInt(dmlen, 10) + dmlenInt, errAtoi := strconv.Atoi(strInt64) + if errAtoi != nil { + return dmClob, errAtoi + } + + // 读取字符串 + str, errReadString := dmClob.ReadString(1, dmlenInt) + + // 处理空字符串或NULL造成的EOF错误 + if errReadString == io.EOF { + return new(string), nil + } + + return &str, errReadString +} +// RegisterCustomDriverValueConver 注册自定义的字段处理逻辑,用于驱动无法直接转换的场景,例如达梦的 TEXT 无法直接转化成 string +// 一般是放到init方法里进行注册 +func init() { + // dialectColumnType 值是 Dialect.字段类型 ,例如 dm.TEXT + zorm.RegisterCustomDriverValueConver("dm.TEXT", CustomDMText{}) +} +``` + +### 金仓(kingbase) +- 配置zorm.DataSourceConfig的 ```DriverName:kingbase ,Dialect:kingbase``` +- 金仓官方驱动: https://www.kingbase.com.cn/qd/index.htm https://bbs.kingbase.com.cn/thread-14457-1-1.html?_dsign=87f12756 +- 金仓kingbase 8核心是基于postgresql 9.6,可以使用 https://github.com/lib/pq 进行测试,生产环境建议使用官方驱动. +- 注意修改数据库的 data/kingbase.conf中 ora_input_emptystr_isnull = false 或者是ora_input_emptystr_isnull = on (根据版本进行区分),因为golang没有null值,一般数据库都是not null,golang的string默认是'',如果这个设置为true,数据库就会把值设置为null,和字段属性not null 冲突,因此报错. + 配置文件修改后,进行数据库的重启. +- 感谢[@Jin](https://gitee.com/GOODJIN) 的测试与建议。 + +### 神通(shentong) +建议使用官方驱动,配置zorm.DataSourceConfig的 ```DriverName:aci ,Dialect:shentong``` + +### 南通(gbase) +~~暂时还未找到官方Go驱动,配置zorm.DataSourceConfig的 DriverName:gbase ,Dialect:gbase~~ +暂时先使用odbc驱动,```DriverName:odbc ,Dialect:gbase``` + +### TDengine +- 因TDengine驱动不支持事务,需要设置```DisableTransaction=true``` +- 配置zorm.DataSourceConfig的 ```DriverName:taosSql或者taosRestful, Dialect:tdengine``` +- zorm.DataSourceConfig的```TDengineInsertsColumnName ```TDengine批量insert语句中是否有列名.默认false没有列名,插入值和数据库列顺序保持一致,减少语句长度 +- 测试用例: https://www.yuque.com/u27016943/nrgi00/dnru3f +- TDengine已收录: https://github.com/taosdata/awesome-tdengine/#orm + +## 数据库脚本和实体类 +生成实体类或手动编写,建议使用代码生成器 https://gitee.com/zhou-a-xing/zorm-generate-struct +```go + +package testzorm + +import ( + "time" + + "gitee.com/chunanyong/zorm" +) + +// 建表语句 + +/* + +DROP TABLE IF EXISTS `t_demo`; +CREATE TABLE `t_demo` ( + `id` varchar(50) NOT NULL COMMENT '主键', + `userName` varchar(30) NOT NULL COMMENT '姓名', + `password` varchar(50) NOT NULL COMMENT '密码', + `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + `active` int COMMENT '是否有效(0否,1是)', + PRIMARY KEY (`id`) +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '例子' ; + +*/ + +// demoStructTableName 表名常量,方便直接调用 +const demoStructTableName = "t_demo" + +// demoStruct 例子 +type demoStruct struct { + // 引入默认的struct,隔离IEntityStruct的方法改动 + zorm.EntityStruct + + // Id 主键 + Id string `column:"id"` + + // UserName 姓名 + UserName string `column:"userName"` + + // Password 密码 + Password string `column:"password"` + + // CreateTime + CreateTime time.Time `column:"createTime"` + + // Active 是否有效(0否,1是) + // Active int `column:"active"` + + // ------------------数据库字段结束,自定义字段写在下面---------------// + // 如果查询的字段在column tag中没有找到,就会根据名称(不区分大小写,支持 _ 下划线转驼峰)映射到struct的属性上 + + // 模拟自定义的字段Active + Active int +} + +// GetTableName 获取表名称 +// IEntityStruct 接口的方法,实体类需要实现!!! +func (entity *demoStruct) GetTableName() string { + return demoStructTableName +} + +// GetPKColumnName 获取数据库表的主键字段名称.因为要兼容Map,只能是数据库的字段名称 +// 不支持联合主键,变通认为无主键,业务控制实现(艰难取舍) +// 如果没有主键,也需要实现这个方法, return "" 即可 +// IEntityStruct 接口的方法,实体类需要实现!!! +func (entity *demoStruct) GetPKColumnName() string { + // 如果没有主键 + // return "" + return "id" +} + +// newDemoStruct 创建一个默认对象 +func newDemoStruct() demoStruct { + demo := demoStruct{ + // 如果Id=="",保存时zorm会调用zorm.FuncGenerateStringID(ctx),默认时间戳+随机数,也可以自己定义实现方式,例如 zorm.FuncGenerateStringID=funcmyId + Id: zorm.FuncGenerateStringID(ctx), + UserName: "defaultUserName", + Password: "defaultPassword", + Active: 1, + CreateTime: time.Now(), + } + return demo +} + + +``` + +## 测试用例即文档 +测试用例: https://gitee.com/wuxiangege/zorm-examples + +```go + +// testzorm 使用原生的sql语句,没有对sql语法做限制.语句使用Finder作为载体 +// 占位符统一使用?,zorm会根据数据库类型,自动替换占位符,例如postgresql数据库把?替换成$1,$2... +// zorm使用 ctx context.Context 参数实现事务传播,ctx从web层传递进来即可,例如gin的c.Request.Context() +// zorm的事务操作需要显式使用zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) {})开启 +package testzorm + +import ( + "context" + "fmt" + "testing" + "time" + + "gitee.com/chunanyong/zorm" + + // 00.引入数据库驱动 + _ "github.com/go-sql-driver/mysql" +) + +// dbDao 代表一个数据库,如果有多个数据库,就对应声明多个DBDao +var dbDao *zorm.DBDao + +// 01.初始化DBDao +func init() { + + // 自定义zorm日志输出 + // zorm.LogCallDepth = 4 // 日志调用的层级 + // zorm.FuncLogError = myFuncLogError // 记录异常日志的函数 + // zorm.FuncLogPanic = myFuncLogPanic // 记录panic日志,默认使用defaultLogError实现 + // zorm.FuncPrintSQL = myFuncPrintSQL // 打印sql的函数 + + // 自定义日志输出格式,把FuncPrintSQL函数重新赋值 + // log.SetFlags(log.LstdFlags) + // zorm.FuncPrintSQL = zorm.FuncPrintSQL + + // 自定义主键生成 + // zorm.FuncGenerateStringID=funcmyId + + // 自定义Tag列名 + // zorm.FuncWrapFieldTagName=funcmyTagName + + // 自定义decimal类型实现,例如github.com/shopspring/decimal + // zorm.FuncDecimalValue=funcmyDecimal + + // Go数据库驱动列表:https://github.com/golang/go/wiki/SQLDrivers + + // dbDaoConfig 数据库的配置.这里只是模拟,生产应该是读取配置配置文件,构造DataSourceConfig + dbDaoConfig := zorm.DataSourceConfig{ + // DSN 数据库的连接字符串,parseTime=true会自动转换为time格式,默认查询出来的是[]byte数组 + DSN: "root:root@tcp(127.0.0.1:3306)/zorm?charset=utf8&parseTime=true", + // DriverName 数据库驱动名称:mysql,postgres,oracle(go-ora),sqlserver,sqlite3,go_ibm_db,clickhouse,dm,kingbase,aci,taosSql|taosRestful 和Dialect对应 + // sql.Open(DriverName,DSN) DriverName就是驱动的sql.Open第一个字符串参数,根据驱动实际情况获取 + DriverName: "mysql", + // Dialect 数据库方言:mysql,postgresql,oracle,mssql,sqlite,db2,clickhouse,dm,kingbase,shentong,tdengine 和 DriverName 对应 + Dialect: "mysql", + // MaxOpenConns 数据库最大连接数 默认50 + MaxOpenConns: 50, + // MaxIdleConns 数据库最大空闲连接数 默认50 + MaxIdleConns: 50, + // ConnMaxLifetimeSecond 连接存活秒时间. 默认600(10分钟)后连接被销毁重建.避免数据库主动断开连接,造成死连接.MySQL默认wait_timeout 28800秒(8小时) + ConnMaxLifetimeSecond: 600, + // SlowSQLMillis 慢sql的时间阈值,单位毫秒.小于0是禁用SQL语句输出;等于0是只输出SQL语句,不计算执行时间;大于0是计算SQL执行时间,并且>=SlowSQLMillis值 + SlowSQLMillis: 0, + // DefaultTxOptions 事务隔离级别的默认配置,默认为nil + // DefaultTxOptions: nil, + // 如果是使用分布式事务,建议使用默认配置 + // DefaultTxOptions: &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}, + + // FuncGlobalTransaction seata/hptx全局分布式事务的适配函数,返回IGlobalTransaction接口的实现 + // 业务必须调用 ctx,_=zorm.BindContextEnableGlobalTransaction(ctx) 开启全局分布事务 + // FuncGlobalTransaction : MyFuncGlobalTransaction, + + // SQLDB 使用现有的数据库连接,优先级高于DSN + // SQLDB : nil, + + // DisableTransaction 禁用事务,默认false,如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务,为了处理某些数据库不支持事务,比如TDengine + // 禁用事务应该有驱动伪造事务API,不应该有orm实现,clickhouse的驱动就是这样做的 + // DisableTransaction :false, + + // TDengineInsertsColumnName TDengine批量insert语句中是否有列名.默认false没有列名,插入值和数据库列顺序保持一致,减少语句长度 + // TDengineInsertsColumnName :false, + } + + // 根据dbDaoConfig创建dbDao, 一个数据库只执行一次,第一个执行的数据库为 defaultDao,后续zorm.xxx方法,默认使用的就是defaultDao + dbDao, _ = zorm.NewDBDao(&dbDaoConfig) +} + +// TestInsert 02.测试保存Struct对象 +func TestInsert(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // 创建一个demo对象 + demo := newDemoStruct() + + // 保存对象,参数是对象指针.如果主键是自增,会赋值到对象的主键属性 + _, err := zorm.Insert(ctx, &demo) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + // 标记测试失败 + if err != nil { + t.Errorf("错误:%v", err) + } +} + +// TestInsertSlice 03.测试批量保存Struct对象的Slice +// 如果是自增主键,无法对Struct对象里的主键属性赋值 +func TestInsertSlice(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // slice存放的类型是zorm.IEntityStruct!!!使用IEntityStruct接口,兼容Struct实体类 + demoSlice := make([]zorm.IEntityStruct, 0) + + // 创建对象1 + demo1 := newDemoStruct() + demo1.UserName = "demo1" + // 创建对象2 + demo2 := newDemoStruct() + demo2.UserName = "demo2" + + demoSlice = append(demoSlice, &demo1, &demo2) + + // 批量保存对象,如果主键是自增,无法保存自增的ID到对象里. + _, err := zorm.InsertSlice(ctx, demoSlice) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + // 标记测试失败 + if err != nil { + t.Errorf("错误:%v", err) + } +} + +// TestInsertEntityMap 04.测试保存EntityMap对象,用于不方便使用struct的场景,使用Map作为载体 +func TestInsertEntityMap(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // 创建一个EntityMap,需要传入表名 + entityMap := zorm.NewEntityMap(demoStructTableName) + // 设置主键名称 + entityMap.PkColumnName = "id" + // 如果是自增序列,设置序列的值 + // entityMap.PkSequence = "mySequence" + + // Set 设置数据库的字段值 + // 如果主键是自增或者序列,不要entityMap.Set主键的值 + entityMap.Set("id", zorm.FuncGenerateStringID(ctx)) + entityMap.Set("userName", "entityMap-userName") + entityMap.Set("password", "entityMap-password") + entityMap.Set("createTime", time.Now()) + entityMap.Set("active", 1) + + // 执行 + _, err := zorm.InsertEntityMap(ctx, entityMap) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + // 标记测试失败 + if err != nil { + t.Errorf("错误:%v", err) + } +} + + +// TestInsertEntityMapSlice 05.测试批量保存[]IEntityMap,用于不方便使用struct的场景,使用Map作为载体 +func TestInsertEntityMapSlice(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + _, err := Transaction(ctx, func(ctx context.Context) (interface{}, error) { + entityMapSlice := make([]IEntityMap, 0) + entityMap1 := NewEntityMap(demoStructTableName) + entityMap1.PkColumnName = "id" + entityMap1.Set("id", zorm.FuncGenerateStringID(ctx)) + entityMap1.Set("userName", "entityMap-userName1") + entityMap1.Set("password", "entityMap-password1") + entityMap1.Set("createTime", time.Now()) + entityMap1.Set("active", 1) + + entityMap2 := NewEntityMap(demoStructTableName) + entityMap2.PkColumnName = "id" + entityMap2.Set("id", zorm.FuncGenerateStringID(ctx)) + entityMap2.Set("userName", "entityMap-userName2") + entityMap2.Set("password", "entityMap-password2") + entityMap2.Set("createTime", time.Now()) + entityMap2.Set("active", 2) + + entityMapSlice = append(entityMapSlice, entityMap1 ,entityMap2) + + // 执行 + _, err := zorm.InsertEntityMapSlice(ctx, entityMapSlice) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + // 标记测试失败 + if err != nil { + t.Errorf("错误:%v", err) + } +} + +// TestQueryRow 06.测试查询一个struct对象 +func TestQueryRow(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 声明一个对象的指针,用于承载返回的数据 + demo := demoStruct{} + + // 构造查询用的finder + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + // finder := zorm.NewSelectFinder(demoStructTableName, "id,userName") // select id,userName from t_demo + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // finder默认启用了sql注入检查,禁止语句中拼接 ' 单引号,可以设置 finder.InjectionCheck = false 解开限制 + + // finder.Append 第一个参数是语句,后面的参数是对应的值,值的顺序要正确.语句统一使用?,zorm会处理数据库的差异 + // in (?) 参数必须有()括号,不能 in ? + finder.Append("WHERE id=? and active in(?)", "20210630163227149563000042432429", []int{0, 1}) + + // 如何使用like + // finder.Append("WHERE id like ? ", "20210630163227149563000042432429%") + + // 执行查询,has为true表示数据库有数据 + has, err := zorm.QueryRow(ctx, finder, &demo) + + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + // 打印结果 + fmt.Println(demo) +} + +// TestQueryRowMap 07.测试查询map接收结果,用于不太适合struct的场景,比较灵活 +func TestQueryRowMap(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 构造查询用的finder + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // finder.Append 第一个参数是语句,后面的参数是对应的值,值的顺序要正确.语句统一使用?,zorm会处理数据库的差异 + // in (?) 参数必须有()括号,不能 in ? + finder.Append("WHERE id=? and active in(?)", "20210630163227149563000042432429", []int{0, 1}) + // 执行查询 + resultMap, err := zorm.QueryRowMap(ctx, finder) + + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + // 打印结果 + fmt.Println(resultMap) +} + +// TestQuery 08.测试查询对象列表 +func TestQuery(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 创建用于接收结果的slice + list := make([]demoStruct, 0) + + // 构造查询用的finder + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + finder := zorm.NewFinder().Append("SELECT id FROM " + demoStructTableName) // select * from t_demo + // 创建分页对象,查询完成后,page对象可以直接给前端分页组件使用 + page := zorm.NewPage() + page.PageNo = 1 // 查询第1页,默认是1 + page.PageSize = 20 // 每页20条,默认是20 + + // 不查询总条数 + // finder.SelectTotalCount = false + + // 如果是特别复杂的语句,造成count语句构造失败,可以手动指定分页语句 + // countFinder := zorm.NewFinder().Append("select count(*) from (") + // countFinder.AppendFinder(finder) + // countFinder.Append(") tempcountfinder") + // finder.CountFinder = countFinder + + // 执行查询 + err := zorm.Query(ctx, finder, &list, page) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + // 打印结果 + fmt.Println("总条数:", page.TotalCount, " 列表:", list) +} + +// TestQueryMap 09.测试查询map列表,用于不方便使用struct的场景,一条记录是一个map对象 +func TestQueryMap(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 构造查询用的finder + // finder := zorm.NewSelectFinder(demoStructTableName) // select * from t_demo + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // 创建分页对象,查询完成后,page对象可以直接给前端分页组件使用 + page := zorm.NewPage() + page.PageNo = 1 // 查询第1页,默认是1 + page.PageSize = 20 // 每页20条,默认是20 + + // 不查询总条数 + // finder.SelectTotalCount = false + + // 如果是特别复杂的语句,造成count语句构造失败,可以手动指定分页语句 + // countFinder := zorm.NewFinder().Append("select count(*) from (") + // countFinder.AppendFinder(finder) + // countFinder.Append(") tempcountfinder") + // finder.CountFinder = countFinder + + // 执行查询 + listMap, err := zorm.QueryMap(ctx, finder, page) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + // 打印结果 + fmt.Println("总条数:", page.TotalCount, " 列表:", listMap) +} + +// TestUpdateNotZeroValue 10.更新struct对象,只更新不为零值的字段.主键必须有值 +func TestUpdateNotZeroValue(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // 声明一个对象的指针,用于更新数据 + demo := demoStruct{} + demo.Id = "20210630163227149563000042432429" + demo.UserName = "UpdateNotZeroValue" + + // 更新 "sql":"UPDATE t_demo SET userName=? WHERE id=?","args":["UpdateNotZeroValue","20210630163227149563000042432429"] + _, err := zorm.UpdateNotZeroValue(ctx, &demo) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + +} + +// TestUpdate 11.更新struct对象,更新所有字段.主键必须有值 +func TestUpdate(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // 声明一个对象的指针,用于更新数据 + demo := demoStruct{} + demo.Id = "20210630163227149563000042432429" + demo.UserName = "TestUpdate" + + _, err := zorm.Update(ctx, &demo) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } +} + +// TestUpdateFinder 12.通过finder更新,zorm最灵活的方式,可以编写任何更新语句,甚至手动编写insert语句 +func TestUpdateFinder(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // finder := zorm.NewUpdateFinder(demoStructTableName) // UPDATE t_demo SET + // finder := zorm.NewDeleteFinder(demoStructTableName) // DELETE FROM t_demo + finder := zorm.NewFinder().Append("UPDATE").Append(demoStructTableName).Append("SET") // UPDATE t_demo SET + finder.Append("userName=?,active=?", "TestUpdateFinder", 1).Append("WHERE id=?", "20210630163227149563000042432429") + + // 更新 "sql":"UPDATE t_demo SET userName=?,active=? WHERE id=?","args":["TestUpdateFinder",1,"20210630163227149563000042432429"] + _, err := zorm.UpdateFinder(ctx, finder) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + +} + +// TestUpdateEntityMap 13.更新一个EntityMap,主键必须有值 +func TestUpdateEntityMap(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + // 创建一个EntityMap,需要传入表名 + entityMap := zorm.NewEntityMap(demoStructTableName) + // 设置主键名称 + entityMap.PkColumnName = "id" + // Set 设置数据库的字段值,主键必须有值 + entityMap.Set("id", "20210630163227149563000042432429") + entityMap.Set("userName", "TestUpdateEntityMap") + // 更新 "sql":"UPDATE t_demo SET userName=? WHERE id=?","args":["TestUpdateEntityMap","20210630163227149563000042432429"] + _, err := zorm.UpdateEntityMap(ctx, entityMap) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + +} + +// TestDelete 14.删除一个struct对象,主键必须有值 +func TestDelete(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 需要手动开启事务,匿名函数返回的error如果不是nil,事务就会回滚.如果设置了DisableTransaction=true,Transaction方法失效,不再要求有事务 + // 如果zorm.DataSourceConfig.DefaultTxOptions配置不满足需求,可以在zorm.Transaction事务方法前设置事务的隔离级别 + // 例如 ctx, _ := dbDao.BindContextTxOptions(ctx, &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}),如果txOptions为nil,使用zorm.DataSourceConfig.DefaultTxOptions + _, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + demo := demoStruct{} + demo.Id = "20210630163227149563000042432429" + + // 删除 "sql":"DELETE FROM t_demo WHERE id=?","args":["20210630163227149563000042432429"] + _, err := zorm.Delete(ctx, &demo) + + // 如果返回的err不是nil,事务就会回滚 + return nil, err + }) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + +} + +// TestProc 15.测试调用存储过程 +func TestProc(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + demo := demoStruct{} + finder := zorm.NewFinder().Append("call testproc(?) ", "u_10001") + zorm.QueryRow(ctx, finder, &demo) + fmt.Println(demo) +} + +// TestFunc 16.测试调用自定义函数 +func TestFunc(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + userName := "" + finder := zorm.NewFinder().Append("select testfunc(?) ", "u_10001") + zorm.QueryRow(ctx, finder, &userName) + fmt.Println(userName) +} + +// TestOther 17.其他的一些说明.非常感谢您能看到这一行 +func TestOther(t *testing.T) { + // ctx 一般一个请求一个ctx,正常应该有web层传入,例如gin的c.Request.Context().这里只是模拟 + var ctx = context.Background() + + // 场景1.多个数据库.通过对应数据库的dbDao,调用BindContextDBConnection函数,把这个数据库的连接绑定到返回的ctx上,然后把ctx传递到zorm的函数即可 + // 也可以重写FuncReadWriteStrategy函数,通过ctx设置不同的key,返回指定数据库的DBDao + newCtx, err := dbDao.BindContextDBConnection(ctx) + if err != nil { // 标记测试失败 + t.Errorf("错误:%v", err) + } + + finder := zorm.NewFinder().Append("SELECT * FROM " + demoStructTableName) // select * from t_demo + // 把新产生的newCtx传递到zorm的函数 + list, _ := zorm.QueryMap(newCtx, finder, nil) + fmt.Println(list) + + // 场景2.单个数据库的读写分离.设置读写分离的策略函数. + zorm.FuncReadWriteStrategy = myReadWriteStrategy + + // 场景3.如果是多个数据库,每个数据库还读写分离,按照 场景1 处理. + // 也可以重写FuncReadWriteStrategy函数,通过ctx设置不同的key,返回指定数据库的DBDao + +} + +// myReadWriteStrategy 数据库的读写分离的策略 rwType=0 read,rwType=1 write +// 也可以通过ctx设置不同的key,返回指定数据库的DBDao +func myReadWriteStrategy(ctx context.Context, rwType int) (*zorm.DBDao, error) { + // 根据自己的业务场景,返回需要的读写dao,每次需要数据库的连接的时候,会调用这个函数 + // if rwType == 0 { + // return dbReadDao + // } + // return dbWriteDao + + return DbDao, nil +} + +// -------------------------------------------- +// ICustomDriverValueConver接口,参见达梦的例子 + +// -------------------------------------------- +// OverrideFunc 重写ZORM的函数,当你使用这个函数时,你必须知道自己在做什么 + +``` +## 分布式事务 +### seata-go CallbackWithCtx函数模式 +```golang +// DataSourceConfig 配置 DefaultTxOptions +// DefaultTxOptions: &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}, + +// 引入seata-go 依赖包 +import ( + "context" + "fmt" + "time" + + "github.com/seata/seata-go/pkg/client" + "github.com/seata/seata-go/pkg/tm" + seataSQL "github.com/seata/seata-go/pkg/datasource/sql" //注意:zorm的 DriverName: seataSQL.SeataATMySQLDriver, !!!! +) + +// 配置文件路径 +var configPath = "./conf/seatago.yml" + +func main() { + // 加载配置文件 + client.InitPath(configPath) + + //初始化zorm数据库 + //注意:zorm的 DriverName: seataSQL.SeataATMySQLDriver, !!!! + initZorm() + + //开启分布式事务 + tm.WithGlobalTx(context.Background(), &tm.GtxConfig{ + Name: "ATSampleLocalGlobalTx", + Timeout: time.Second * 30, + }, CallbackWithCtx) + // CallbackWithCtx business callback definition + // type CallbackWithCtx func(ctx context.Context) error + + + // 事务开启之后获取XID.可以通过gin的header传递,或者其他方式传递 + // xid:=tm.GetXID(ctx) + // tm.SetXID(ctx, xid) + + // 如果使用的gin框架,可以使用中间件绑定参数 + // r.Use(ginmiddleware.TransactionMiddleware()) + +} +``` + +### seata-go 事务托管模式 + +```golang + +// 不使用seata-go CallbackWithCtx函数,zorm实现事务托管,不修改业务代码,零侵入实现分布式事务 + +// 必须手动开启分布式事务,必须放到本地事务开启之前调用 +ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) +// 分布式事务示例代码 +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // 获取当前分布式事务的XID.不用考虑怎么来的,如果是分布式事务环境,会自动设置值 + // xid := ctx.Value("XID").(string) + + // 把xid传递到第三方应用 + // req.Header.Set("XID", xid) + + // 如果返回的err不是nil,本地事务和分布式事务就会回滚 + return nil, err +}) + +// /----------第三方应用-------/ // + + // 不要使用seata-go默认提供的中间件,只需要ctx绑定XID即可 !!! + //// r.Use(ginmiddleware.TransactionMiddleware()) + xid := c.GetHeader(constant.XidKey) + ctx = context.WithValue(ctx, "XID", xid) + + // 必须手动开启分布式事务,必须放到本地事务开启之前调用 + ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) + // ctx绑定XID之后,调用业务事务 +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // 业务代码...... + + // 如果返回的err不是nil,本地事务和分布式事务就会回滚 + return nil, err +}) + + + +// 建议以下代码放到单独的文件里 +// ................// + + +// ZormGlobalTransaction 包装seata-go的*tm.GlobalTransactionManager,实现zorm.IGlobalTransaction接口 +type ZormGlobalTransaction struct { + *tm.GlobalTransactionManager +} + +// MyFuncGlobalTransaction zorm适配seata-go全局分布式事务的函数 +// 重要!!!!需要配置zorm.DataSourceConfig.FuncGlobalTransaction=MyFuncGlobalTransaction 重要!!! +func MyFuncGlobalTransaction(ctx context.Context) (zorm.IGlobalTransaction, context.Context, context.Context, error) { + // 创建seata-go事务 + globalTx := tm.GetGlobalTransactionManager() + // 使用zorm.IGlobalTransaction接口对象包装分布式事务,隔离seata-go依赖 + globalTransaction := &ZormGlobalTransaction{globalTx} + + if tm.IsSeataContext(ctx) { + return globalTransaction, ctx, ctx, nil + } + // open global transaction for the first time + ctx = tm.InitSeataContext(ctx) + // 有请求传入,手动获取的XID + xidObj := ctx.Value("XID") + if xidObj != nil { + xid := xidObj.(string) + tm.SetXID(ctx, xid) + } + tm.SetTxName(ctx, "ATSampleLocalGlobalTx") + + // use new context to process current global transaction. + if tm.IsGlobalTx(ctx) { + globalRootContext := transferTx(ctx) + return globalTransaction, ctx, globalRootContext, nil + } + return globalTransaction, ctx, ctx, nil +} + +// 实现zorm.IGlobalTransaction 托管全局分布式事务接口 +// BeginGTX 开启全局分布式事务 +func (gtx *ZormGlobalTransaction) BeginGTX(ctx context.Context, globalRootContext context.Context) error { + //tm.SetTxStatus(globalRootContext, message.GlobalStatusBegin) + err := gtx.Begin(globalRootContext, time.Second*30) + return err +} + +// CommitGTX 提交全局分布式事务 +func (gtx *ZormGlobalTransaction) CommitGTX(ctx context.Context, globalRootContext context.Context) error { + gtr := tm.GetTx(globalRootContext) + return gtx.Commit(globalRootContext, gtr) +} + +// RollbackGTX 回滚全局分布式事务 +func (gtx *ZormGlobalTransaction) RollbackGTX(ctx context.Context, globalRootContext context.Context) error { + gtr := tm.GetTx(globalRootContext) + // 如果是Participant角色,修改为Launcher角色,允许分支事务提交全局事务. + if gtr.TxRole != tm.Launcher { + gtr.TxRole = tm.Launcher + } + return gtx.Rollback(globalRootContext, gtr) +} + +// GetGTXID 获取全局分布式事务的XID +func (gtx *ZormGlobalTransaction) GetGTXID(ctx context.Context, globalRootContext context.Context) (string, error) { + return tm.GetXID(globalRootContext), nil +} + +// transferTx transfer the gtx into a new ctx from old ctx. +// use it to implement suspend and resume instead of seata java +func transferTx(ctx context.Context) context.Context { + newCtx := tm.InitSeataContext(context.Background()) + tm.SetXID(newCtx, tm.GetXID(ctx)) + return newCtx +} + +// ................// +``` + +### hptx proxy模式 + +hptx已合并[@小口天](https://gitee.com/wuxiangege)的pr, [在hptx代理模式下的zorm使用示例](https://github.com/CECTC/hptx-samples/tree/main/http_proxy_zorm) + +```golang +// DataSourceConfig 配置 DefaultTxOptions +// DefaultTxOptions: &sql.TxOptions{Isolation: sql.LevelDefault, ReadOnly: false}, + +// 引入hptx 依赖包 +import ( + "github.com/cectc/hptx" + "github.com/cectc/hptx/pkg/config" + "github.com/cectc/hptx/pkg/resource" + "github.com/cectc/mysql" + "github.com/cectc/hptx/pkg/tm" + + gtxContext "github.com/cectc/hptx/pkg/base/context" +) + +// 配置文件路径 +var configPath = "./conf/config.yml" + +func main() { + + // 初始化配置 + hptx.InitFromFile(configPath) + + // 注册mysql驱动 + mysql.RegisterResource(config.GetATConfig().DSN) + resource.InitATBranchResource(mysql.GetDataSourceManager()) + // sqlDB, err := sql.Open("mysql", config.GetATConfig().DSN) + + + // 后续正常初始化zorm,一定要放到hptx mysql 初始化后面!!! + + // ................// + // tm注册事务服务,参照官方例子.(事务托管主要是去掉proxy,对业务零侵入) + tm.Implement(svc.ProxySvc) + // ................// + + + // 获取hptx的rootContext + // rootContext := gtxContext.NewRootContext(ctx) + // rootContext := ctx.(*gtxContext.RootContext) + + // 创建hptx事务 + // globalTx := tm.GetCurrentOrCreate(rootContext) + + // 开始事务 + // globalTx.BeginWithTimeoutAndName(int32(6000), "事务名称", rootContext) + + // 事务开启之后获取XID.可以通过gin的header传递,或者其他方式传递 + // xid:=rootContext.GetXID() + + // 如果使用的gin框架,获取到ctx + // ctx := c.Request.Context() + + // 接受传递过来的XID,绑定到本地ctx + // ctx =context.WithValue(ctx,mysql.XID,xid) +} +``` + +### hptx 事务托管模式 + +hptx已合并[@小口天](https://gitee.com/wuxiangege)的pr, [zorm事务托管hptx示例](https://github.com/CECTC/hptx-samples/tree/main/http_zorm) + +```golang + +// 不使用proxy代理模式,zorm实现事务托管,不修改业务代码,零侵入实现分布式事务 +// tm.Implement(svc.ProxySvc) + +// 必须手动开启分布式事务,必须放到本地事务开启之前调用 +ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) +// 分布式事务示例代码 +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // 获取当前分布式事务的XID.不用考虑怎么来的,如果是分布式事务环境,会自动设置值 + // xid := ctx.Value("XID").(string) + + // 把xid传递到第三方应用 + // req.Header.Set("XID", xid) + + // 如果返回的err不是nil,本地事务和分布式事务就会回滚 + return nil, err +}) + +// /----------第三方应用-------// / + +// 第三方应用开启事务前,ctx需要绑定XID,例如使用了gin框架 + +// 接受传递过来的XID,绑定到本地ctx +// xid:=c.Request.Header.Get("XID") +// 获取到ctx +// ctx := c.Request.Context() +// ctx = context.WithValue(ctx,"XID",xid) + +// 必须手动开启分布式事务,必须放到本地事务开启之前调用 +ctx,_ = zorm.BindContextEnableGlobalTransaction(ctx) +// ctx绑定XID之后,调用业务事务 +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // 业务代码...... + + // 如果返回的err不是nil,本地事务和分布式事务就会回滚 + return nil, err +}) + + +// 建议以下代码放到单独的文件里 +// ................// + +// ZormGlobalTransaction 包装hptx的*tm.DefaultGlobalTransaction,实现zorm.IGlobalTransaction接口 +type ZormGlobalTransaction struct { + *tm.DefaultGlobalTransaction +} + +// MyFuncGlobalTransaction zorm适配hptx 全局分布式事务的函数 +// 重要!!!!需要配置zorm.DataSourceConfig.FuncGlobalTransaction=MyFuncGlobalTransaction 重要!!! +func MyFuncGlobalTransaction(ctx context.Context) (zorm.IGlobalTransaction, context.Context, context.Context, error) { + // 获取hptx的rootContext + rootContext := gtxContext.NewRootContext(ctx) + // 创建hptx事务 + globalTx := tm.GetCurrentOrCreate(rootContext) + // 使用zorm.IGlobalTransaction接口对象包装分布式事务,隔离hptx依赖 + globalTransaction := &ZormGlobalTransaction{globalTx} + + return globalTransaction, ctx, rootContext, nil +} + + +// 实现zorm.IGlobalTransaction 托管全局分布式事务接口 +// BeginGTX 开启全局分布式事务 +func (gtx *ZormGlobalTransaction) BeginGTX(ctx context.Context, globalRootContext context.Context) error { + rootContext := globalRootContext.(*gtxContext.RootContext) + return gtx.BeginWithTimeout(int32(6000), rootContext) +} + +// CommitGTX 提交全局分布式事务 +func (gtx *ZormGlobalTransaction) CommitGTX(ctx context.Context, globalRootContext context.Context) error { + rootContext := globalRootContext.(*gtxContext.RootContext) + return gtx.Commit(rootContext) +} + +// RollbackGTX 回滚全局分布式事务 +func (gtx *ZormGlobalTransaction) RollbackGTX(ctx context.Context, globalRootContext context.Context) error { + rootContext := globalRootContext.(*gtxContext.RootContext) + // 如果是Participant角色,修改为Launcher角色,允许分支事务提交全局事务. + if gtx.Role != tm.Launcher { + gtx.Role = tm.Launcher + } + return gtx.Rollback(rootContext) +} +// GetGTXID 获取全局分布式事务的XID +func (gtx *ZormGlobalTransaction) GetGTXID(ctx context.Context, globalRootContext context.Context) (string,error) { + rootContext := globalRootContext.(*gtxContext.RootContext) + return rootContext.GetXID(), nil +} + +// ................// +``` + + + +### dbpack分布式事务 +```dbpack``` 文档:https://cectc.github.io/dbpack-doc/#/README +使用 Mesh 方式部署,对应用集成比较简单,只需要获取xid,放到sql语句的hint就可以了 +```golang +// 开启dbpack事务前,ctx需要绑定sql hint,例如使用gin框架获取header传递过来的xid +xid := c.Request.Header.Get("xid") +// 使用xid生成sql的hint内容,然后将hint绑定到ctx +hint := fmt.Sprintf("/*+ XID('%s') */", xid) +// 获取到ctx +ctx := c.Request.Context() +// 将hint绑定到ctx +ctx,_ = zorm.BindContextSQLHint(ctx,hint) + +// ctx绑定sql hint之后,调用业务事务,传递ctx实现分布式事务的传播 +_, err := zorm.Transaction(ctx, func(ctx context.Context) (interface{}, error) { + + // 业务代码...... + + // 如果返回的err不是nil,本地事务和分布式事务就会回滚 + return nil, err +}) + +``` + + diff --git a/vendor/gitee.com/chunanyong/zorm/dataSource.go b/vendor/gitee.com/chunanyong/zorm/dataSource.go new file mode 100644 index 00000000..b9d09142 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/dataSource.go @@ -0,0 +1,318 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" +) + +// dataSorce对象,隔离sql原生对象 +// dataSorce Isolate sql native objects +type dataSource struct { + *sql.DB + // config *DataSourceConfig +} + +// newDataSource 创建一个新的datasource,内部调用,避免外部直接使用datasource +// newDAtaSource Create a new datasource and call it internally to avoid direct external use of the datasource +func newDataSource(config *DataSourceConfig) (*dataSource, error) { + if config == nil { + return nil, errors.New("->newDataSource-->config cannot be nil") + } + + if config.DriverName == "" { + return nil, errors.New("->newDataSource-->DriverName cannot be empty") + } + // 兼容处理,DBType即将废弃,请使用Dialect属性 + if config.DBType != "" && config.Dialect == "" { + FuncLogError(nil, errors.New("->newDataSource-->DataSourceConfig的DBType即将废弃,请使用Dialect属性")) + config.Dialect = config.DBType + } + if config.Dialect == "" { + return nil, errors.New("->newDataSource-->Dialect cannot be empty") + } + var db *sql.DB + var errSQLOpen error + + if config.SQLDB == nil { // 没有已经存在的数据库连接,使用DSN初始化 + if config.DSN == "" { + return nil, errors.New("->newDataSource-->DSN cannot be empty") + } + db, errSQLOpen = sql.Open(config.DriverName, config.DSN) + if errSQLOpen != nil { + errSQLOpen = fmt.Errorf("->newDataSource-->open数据库打开失败:%w", errSQLOpen) + FuncLogError(nil, errSQLOpen) + return nil, errSQLOpen + } + } else { // 使用已经存在的数据库连接 + db = config.SQLDB + } + + if config.MaxOpenConns == 0 { + config.MaxOpenConns = 50 + } + if config.MaxIdleConns == 0 { + config.MaxIdleConns = 50 + } + + if config.ConnMaxLifetimeSecond == 0 { + config.ConnMaxLifetimeSecond = 600 + } + + // 设置数据库最大连接数 + // Set the maximum number of database connections + db.SetMaxOpenConns(config.MaxOpenConns) + // 设置数据库最大空闲连接数 + // Set the maximum number of free connections to the database + db.SetMaxIdleConns(config.MaxIdleConns) + //连接存活秒时间. 默认600(10分钟)后连接被销毁重建.避免数据库主动断开连接,造成死连接.MySQL默认wait_timeout 28800秒(8小时) + //(Connection survival time in seconds) Destroy and rebuild the connection after the default 600 seconds (10 minutes) + //Prevent the database from actively disconnecting and causing dead connections. MySQL Default wait_timeout 28800 seconds + db.SetConnMaxLifetime(time.Second * time.Duration(config.ConnMaxLifetimeSecond)) + + // 验证连接 + if pingerr := db.Ping(); pingerr != nil { + pingerr = fmt.Errorf("->newDataSource-->ping数据库失败:%w", pingerr) + FuncLogError(nil, pingerr) + db.Close() + return nil, pingerr + } + + return &dataSource{db}, nil +} + +// 事务参照:https://www.jianshu.com/p/2a144332c3db +// Transaction reference: https://www.jianshu.com/p/2a144332c3db + +// dataBaseConnection 数据库dbConnection会话,可以原生查询或者事务 +// dataBaseConnection Database session, native query or transaction. +type dataBaseConnection struct { + // 原生db + // native db + db *sql.DB + + // 原生事务 + // native transaction + tx *sql.Tx + + // 数据库配置 + config *DataSourceConfig +} + +// beginTx 开启事务 +// beginTx Open transaction +func (dbConnection *dataBaseConnection) beginTx(ctx context.Context) error { + if dbConnection.tx != nil { + return nil + } + // 设置事务配置,主要是隔离级别 + var txOptions *sql.TxOptions + contextTxOptions := ctx.Value(contextTxOptionsKey) + if contextTxOptions != nil { + txOptions, _ = contextTxOptions.(*sql.TxOptions) + } else { + txOptions = dbConnection.config.DefaultTxOptions + } + + tx, err := dbConnection.db.BeginTx(ctx, txOptions) + if err != nil { + err = fmt.Errorf("->beginTx事务开启失败:%w", err) + return err + } + dbConnection.tx = tx + return nil +} + +// rollback 回滚事务 +// rollback Rollback transaction +func (dbConnection *dataBaseConnection) rollback() error { + if dbConnection.tx == nil { + return nil + } + + err := dbConnection.tx.Rollback() + if err != nil { + err = fmt.Errorf("->rollback事务回滚失败:%w", err) + return err + } + dbConnection.tx = nil + return nil +} + +// commit 提交事务 +// commit Commit transaction +func (dbConnection *dataBaseConnection) commit() error { + if dbConnection.tx == nil { + return errors.New("->dbConnection.commit()事务为空") + } + + err := dbConnection.tx.Commit() + if err != nil { + err = fmt.Errorf("->dbConnection.commit()事务提交失败:%w", err) + return err + } + dbConnection.tx = nil + return nil +} + +// execContext 执行sql语句,如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 +// execContext Execute sql statement,If the transaction has been opened,it will be executed in transaction mode, if the transaction is not opened,it will be executed in non-transactional mode +func (dbConnection *dataBaseConnection) execContext(ctx context.Context, sqlstr *string, argsValues *[]interface{}) (*sql.Result, error) { + // reBindSQL 重新处理参数代入方式 + execsql, args, err := reBindSQL(dbConnection.config.Dialect, sqlstr, argsValues) + if err != nil { + return nil, err + } + // 更新语句处理ClickHouse特殊语法 + err = reUpdateSQL(dbConnection.config.Dialect, execsql) + if err != nil { + return nil, err + } + // 执行前加入 hint + err = wrapSQLHint(ctx, execsql) + if err != nil { + return nil, err + } + var start *time.Time + var res sql.Result + // 小于0是禁用日志输出;等于0是只输出日志,不计算SQ执行时间;大于0是计算执行时间,并且大于指定值 + slowSQLMillis := dbConnection.config.SlowSQLMillis + if slowSQLMillis == 0 { + FuncPrintSQL(ctx, *execsql, *args, 0) + } else if slowSQLMillis > 0 { + now := time.Now() // 获取当前时间 + start = &now + } + if dbConnection.tx != nil { + res, err = dbConnection.tx.ExecContext(ctx, *execsql, *args...) + } else { + res, err = dbConnection.db.ExecContext(ctx, *execsql, *args...) + } + if slowSQLMillis > 0 { + slow := time.Since(*start).Milliseconds() + if slow-int64(slowSQLMillis) >= 0 { + FuncPrintSQL(ctx, *execsql, *args, slow) + } + } + if err != nil { + err = fmt.Errorf("->execContext执行错误:%w,-->zormErrorExecSQL:%s,-->zormErrorSQLValues:%v", err, *execsql, *args) + } + return &res, err +} + +// queryRowContext 如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 +func (dbConnection *dataBaseConnection) queryRowContext(ctx context.Context, sqlstr *string, argsValues *[]interface{}) (*sql.Row, error) { + // reBindSQL 重新处理参数代入方式 + query, args, err := reBindSQL(dbConnection.config.Dialect, sqlstr, argsValues) + if err != nil { + return nil, err + } + // 执行前加入 hint + err = wrapSQLHint(ctx, query) + if err != nil { + return nil, err + } + var start *time.Time + var row *sql.Row + // 小于0是禁用日志输出;等于0是只输出日志,不计算SQ执行时间;大于0是计算执行时间,并且大于指定值 + slowSQLMillis := dbConnection.config.SlowSQLMillis + if slowSQLMillis == 0 { + FuncPrintSQL(ctx, *query, *args, 0) + } else if slowSQLMillis > 0 { + now := time.Now() // 获取当前时间 + start = &now + } + + if dbConnection.tx != nil { + row = dbConnection.tx.QueryRowContext(ctx, *query, *args...) + } else { + row = dbConnection.db.QueryRowContext(ctx, *query, *args...) + } + if slowSQLMillis > 0 { + slow := time.Since(*start).Milliseconds() + if slow-int64(slowSQLMillis) >= 0 { + FuncPrintSQL(ctx, *query, *args, slow) + } + } + return row, nil +} + +// queryContext 查询数据,如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 +// queryRowContext Execute sql row statement,If the transaction has been opened,it will be executed in transaction mode, if the transaction is not opened,it will be executed in non-transactional mode +func (dbConnection *dataBaseConnection) queryContext(ctx context.Context, sqlstr *string, argsValues *[]interface{}) (*sql.Rows, error) { + // reBindSQL 重新处理参数代入方式 + query, args, err := reBindSQL(dbConnection.config.Dialect, sqlstr, argsValues) + if err != nil { + return nil, err + } + // 执行前加入 hint + err = wrapSQLHint(ctx, query) + if err != nil { + return nil, err + } + var start *time.Time + var rows *sql.Rows + // 小于0是禁用日志输出;等于0是只输出日志,不计算SQ执行时间;大于0是计算执行时间,并且大于指定值 + slowSQLMillis := dbConnection.config.SlowSQLMillis + if slowSQLMillis == 0 { + FuncPrintSQL(ctx, *query, *args, 0) + } else if slowSQLMillis > 0 { + now := time.Now() // 获取当前时间 + start = &now + } + + if dbConnection.tx != nil { + rows, err = dbConnection.tx.QueryContext(ctx, *query, *args...) + } else { + rows, err = dbConnection.db.QueryContext(ctx, *query, *args...) + } + if slowSQLMillis > 0 { + slow := time.Since(*start).Milliseconds() + if slow-int64(slowSQLMillis) >= 0 { + FuncPrintSQL(ctx, *query, *args, slow) + } + } + if err != nil { + err = fmt.Errorf("->queryContext执行错误:%w,-->zormErrorExecSQL:%s,-->zormErrorSQLValues:%v", err, *query, *args) + } + return rows, err +} + +/* +// prepareContext 预执行,如果已经开启事务,就以事务方式执行,如果没有开启事务,就以非事务方式执行 +// prepareContext Pre-execution,If the transaction has been opened,it will be executed in transaction mode,if the transaction is not opened,it will be executed in non-transactional mode +func (dbConnection *dataBaseConnection) prepareContext(ctx context.Context, query *string) (*sql.Stmt, error) { + //打印SQL + //print SQL + if dbConnection.config.PrintSQL { + //logger.Info("printSQL", logger.String("sql", query)) + FuncPrintSQL(ctx,*query, nil) + } + + if dbConnection.tx != nil { + return dbConnection.tx.PrepareContext(ctx, *query) + } + + return dbConnection.db.PrepareContext(ctx, *query) +} +*/ diff --git a/vendor/gitee.com/chunanyong/zorm/decimal/decimal-go.go b/vendor/gitee.com/chunanyong/zorm/decimal/decimal-go.go new file mode 100644 index 00000000..9958d690 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/decimal/decimal-go.go @@ -0,0 +1,415 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Multiprecision decimal numbers. +// For floating-point formatting only; not general purpose. +// Only operations are assign and (binary) left/right shift. +// Can do binary floating point in multiprecision decimal precisely +// because 2 divides 10; cannot do decimal floating point +// in multiprecision binary precisely. + +package decimal + +type decimal struct { + d [800]byte // digits, big-endian representation + nd int // number of digits used + dp int // decimal point + neg bool // negative flag + trunc bool // discarded nonzero digits beyond d[:nd] +} + +func (a *decimal) String() string { + n := 10 + a.nd + if a.dp > 0 { + n += a.dp + } + if a.dp < 0 { + n += -a.dp + } + + buf := make([]byte, n) + w := 0 + switch { + case a.nd == 0: + return "0" + + case a.dp <= 0: + // zeros fill space between decimal point and digits + buf[w] = '0' + w++ + buf[w] = '.' + w++ + w += digitZero(buf[w : w+-a.dp]) + w += copy(buf[w:], a.d[0:a.nd]) + + case a.dp < a.nd: + // decimal point in middle of digits + w += copy(buf[w:], a.d[0:a.dp]) + buf[w] = '.' + w++ + w += copy(buf[w:], a.d[a.dp:a.nd]) + + default: + // zeros fill space between digits and decimal point + w += copy(buf[w:], a.d[0:a.nd]) + w += digitZero(buf[w : w+a.dp-a.nd]) + } + return string(buf[0:w]) +} + +func digitZero(dst []byte) int { + for i := range dst { + dst[i] = '0' + } + return len(dst) +} + +// trim trailing zeros from number. +// (They are meaningless; the decimal point is tracked +// independent of the number of digits.) +func trim(a *decimal) { + for a.nd > 0 && a.d[a.nd-1] == '0' { + a.nd-- + } + if a.nd == 0 { + a.dp = 0 + } +} + +// Assign v to a. +func (a *decimal) Assign(v uint64) { + var buf [24]byte + + // Write reversed decimal in buf. + n := 0 + for v > 0 { + v1 := v / 10 + v -= 10 * v1 + buf[n] = byte(v + '0') + n++ + v = v1 + } + + // Reverse again to produce forward decimal in a.d. + a.nd = 0 + for n--; n >= 0; n-- { + a.d[a.nd] = buf[n] + a.nd++ + } + a.dp = a.nd + trim(a) +} + +// Maximum shift that we can do in one pass without overflow. +// A uint has 32 or 64 bits, and we have to be able to accommodate 9<> 63) +const maxShift = uintSize - 4 + +// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow. +func rightShift(a *decimal, k uint) { + r := 0 // read pointer + w := 0 // write pointer + + // Pick up enough leading digits to cover first shift. + var n uint + for ; n>>k == 0; r++ { + if r >= a.nd { + if n == 0 { + // a == 0; shouldn't get here, but handle anyway. + a.nd = 0 + return + } + for n>>k == 0 { + n = n * 10 + r++ + } + break + } + c := uint(a.d[r]) + n = n*10 + c - '0' + } + a.dp -= r - 1 + + var mask uint = (1 << k) - 1 + + // Pick up a digit, put down a digit. + for ; r < a.nd; r++ { + c := uint(a.d[r]) + dig := n >> k + n &= mask + a.d[w] = byte(dig + '0') + w++ + n = n*10 + c - '0' + } + + // Put down extra digits. + for n > 0 { + dig := n >> k + n &= mask + if w < len(a.d) { + a.d[w] = byte(dig + '0') + w++ + } else if dig > 0 { + a.trunc = true + } + n = n * 10 + } + + a.nd = w + trim(a) +} + +// Cheat sheet for left shift: table indexed by shift count giving +// number of new digits that will be introduced by that shift. +// +// For example, leftcheats[4] = {2, "625"}. That means that +// if we are shifting by 4 (multiplying by 16), it will add 2 digits +// when the string prefix is "625" through "999", and one fewer digit +// if the string prefix is "000" through "624". +// +// Credit for this trick goes to Ken. + +type leftCheat struct { + delta int // number of new digits + cutoff string // minus one digit if original < a. +} + +var leftcheats = []leftCheat{ + // Leading digits of 1/2^i = 5^i. + // 5^23 is not an exact 64-bit floating point number, + // so have to use bc for the math. + // Go up to 60 to be large enough for 32bit and 64bit platforms. + /* + seq 60 | sed 's/^/5^/' | bc | + awk 'BEGIN{ print "\t{ 0, \"\" }," } + { + log2 = log(2)/log(10) + printf("\t{ %d, \"%s\" },\t// * %d\n", + int(log2*NR+1), $0, 2**NR) + }' + */ + {0, ""}, + {1, "5"}, // * 2 + {1, "25"}, // * 4 + {1, "125"}, // * 8 + {2, "625"}, // * 16 + {2, "3125"}, // * 32 + {2, "15625"}, // * 64 + {3, "78125"}, // * 128 + {3, "390625"}, // * 256 + {3, "1953125"}, // * 512 + {4, "9765625"}, // * 1024 + {4, "48828125"}, // * 2048 + {4, "244140625"}, // * 4096 + {4, "1220703125"}, // * 8192 + {5, "6103515625"}, // * 16384 + {5, "30517578125"}, // * 32768 + {5, "152587890625"}, // * 65536 + {6, "762939453125"}, // * 131072 + {6, "3814697265625"}, // * 262144 + {6, "19073486328125"}, // * 524288 + {7, "95367431640625"}, // * 1048576 + {7, "476837158203125"}, // * 2097152 + {7, "2384185791015625"}, // * 4194304 + {7, "11920928955078125"}, // * 8388608 + {8, "59604644775390625"}, // * 16777216 + {8, "298023223876953125"}, // * 33554432 + {8, "1490116119384765625"}, // * 67108864 + {9, "7450580596923828125"}, // * 134217728 + {9, "37252902984619140625"}, // * 268435456 + {9, "186264514923095703125"}, // * 536870912 + {10, "931322574615478515625"}, // * 1073741824 + {10, "4656612873077392578125"}, // * 2147483648 + {10, "23283064365386962890625"}, // * 4294967296 + {10, "116415321826934814453125"}, // * 8589934592 + {11, "582076609134674072265625"}, // * 17179869184 + {11, "2910383045673370361328125"}, // * 34359738368 + {11, "14551915228366851806640625"}, // * 68719476736 + {12, "72759576141834259033203125"}, // * 137438953472 + {12, "363797880709171295166015625"}, // * 274877906944 + {12, "1818989403545856475830078125"}, // * 549755813888 + {13, "9094947017729282379150390625"}, // * 1099511627776 + {13, "45474735088646411895751953125"}, // * 2199023255552 + {13, "227373675443232059478759765625"}, // * 4398046511104 + {13, "1136868377216160297393798828125"}, // * 8796093022208 + {14, "5684341886080801486968994140625"}, // * 17592186044416 + {14, "28421709430404007434844970703125"}, // * 35184372088832 + {14, "142108547152020037174224853515625"}, // * 70368744177664 + {15, "710542735760100185871124267578125"}, // * 140737488355328 + {15, "3552713678800500929355621337890625"}, // * 281474976710656 + {15, "17763568394002504646778106689453125"}, // * 562949953421312 + {16, "88817841970012523233890533447265625"}, // * 1125899906842624 + {16, "444089209850062616169452667236328125"}, // * 2251799813685248 + {16, "2220446049250313080847263336181640625"}, // * 4503599627370496 + {16, "11102230246251565404236316680908203125"}, // * 9007199254740992 + {17, "55511151231257827021181583404541015625"}, // * 18014398509481984 + {17, "277555756156289135105907917022705078125"}, // * 36028797018963968 + {17, "1387778780781445675529539585113525390625"}, // * 72057594037927936 + {18, "6938893903907228377647697925567626953125"}, // * 144115188075855872 + {18, "34694469519536141888238489627838134765625"}, // * 288230376151711744 + {18, "173472347597680709441192448139190673828125"}, // * 576460752303423488 + {19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976 +} + +// Is the leading prefix of b lexicographically less than s? +func prefixIsLessThan(b []byte, s string) bool { + for i := 0; i < len(s); i++ { + if i >= len(b) { + return true + } + if b[i] != s[i] { + return b[i] < s[i] + } + } + return false +} + +// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow. +func leftShift(a *decimal, k uint) { + delta := leftcheats[k].delta + if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) { + delta-- + } + + r := a.nd // read index + w := a.nd + delta // write index + + // Pick up a digit, put down a digit. + var n uint + for r--; r >= 0; r-- { + n += (uint(a.d[r]) - '0') << k + quo := n / 10 + rem := n - 10*quo + w-- + if w < len(a.d) { + a.d[w] = byte(rem + '0') + } else if rem != 0 { + a.trunc = true + } + n = quo + } + + // Put down extra digits. + for n > 0 { + quo := n / 10 + rem := n - 10*quo + w-- + if w < len(a.d) { + a.d[w] = byte(rem + '0') + } else if rem != 0 { + a.trunc = true + } + n = quo + } + + a.nd += delta + if a.nd >= len(a.d) { + a.nd = len(a.d) + } + a.dp += delta + trim(a) +} + +// Binary shift left (k > 0) or right (k < 0). +func (a *decimal) Shift(k int) { + switch { + case a.nd == 0: + // nothing to do: a == 0 + case k > 0: + for k > maxShift { + leftShift(a, maxShift) + k -= maxShift + } + leftShift(a, uint(k)) + case k < 0: + for k < -maxShift { + rightShift(a, maxShift) + k += maxShift + } + rightShift(a, uint(-k)) + } +} + +// If we chop a at nd digits, should we round up? +func shouldRoundUp(a *decimal, nd int) bool { + if nd < 0 || nd >= a.nd { + return false + } + if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even + // if we truncated, a little higher than what's recorded - always round up + if a.trunc { + return true + } + return nd > 0 && (a.d[nd-1]-'0')%2 != 0 + } + // not halfway - digit tells all + return a.d[nd] >= '5' +} + +// Round a to nd digits (or fewer). +// If nd is zero, it means we're rounding +// just to the left of the digits, as in +// 0.09 -> 0.1. +func (a *decimal) Round(nd int) { + if nd < 0 || nd >= a.nd { + return + } + if shouldRoundUp(a, nd) { + a.RoundUp(nd) + } else { + a.RoundDown(nd) + } +} + +// Round a down to nd digits (or fewer). +func (a *decimal) RoundDown(nd int) { + if nd < 0 || nd >= a.nd { + return + } + a.nd = nd + trim(a) +} + +// Round a up to nd digits (or fewer). +func (a *decimal) RoundUp(nd int) { + if nd < 0 || nd >= a.nd { + return + } + + // round up + for i := nd - 1; i >= 0; i-- { + c := a.d[i] + if c < '9' { // can stop after this digit + a.d[i]++ + a.nd = i + 1 + return + } + } + + // Number is all 9s. + // Change to single 1 with adjusted decimal point. + a.d[0] = '1' + a.nd = 1 + a.dp++ +} + +// Extract integer part, rounded appropriately. +// No guarantees about overflow. +func (a *decimal) RoundedInteger() uint64 { + if a.dp > 20 { + return 0xFFFFFFFFFFFFFFFF + } + var i int + n := uint64(0) + for i = 0; i < a.dp && i < a.nd; i++ { + n = n*10 + uint64(a.d[i]-'0') + } + for ; i < a.dp; i++ { + n *= 10 + } + if shouldRoundUp(a, a.dp) { + n++ + } + return n +} diff --git a/vendor/gitee.com/chunanyong/zorm/decimal/decimal.go b/vendor/gitee.com/chunanyong/zorm/decimal/decimal.go new file mode 100644 index 00000000..c614ea79 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/decimal/decimal.go @@ -0,0 +1,1904 @@ +// Package decimal implements an arbitrary precision fixed-point decimal. +// +// The zero-value of a Decimal is 0, as you would expect. +// +// The best way to create a new Decimal is to use decimal.NewFromString, ex: +// +// n, err := decimal.NewFromString("-123.4567") +// n.String() // output: "-123.4567" +// +// To use Decimal as part of a struct: +// +// type Struct struct { +// Number Decimal +// } +// +// Note: This can "only" represent numbers with a maximum of 2^31 digits after the decimal point. +package decimal + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + "math" + "math/big" + "regexp" + "strconv" + "strings" +) + +// DivisionPrecision is the number of decimal places in the result when it +// doesn't divide exactly. +// +// Example: +// +// d1 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3)) +// d1.String() // output: "0.6666666666666667" +// d2 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(30000)) +// d2.String() // output: "0.0000666666666667" +// d3 := decimal.NewFromFloat(20000).Div(decimal.NewFromFloat(3)) +// d3.String() // output: "6666.6666666666666667" +// decimal.DivisionPrecision = 3 +// d4 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3)) +// d4.String() // output: "0.667" +// +var DivisionPrecision = 16 + +// MarshalJSONWithoutQuotes should be set to true if you want the decimal to +// be JSON marshaled as a number, instead of as a string. +// WARNING: this is dangerous for decimals with many digits, since many JSON +// unmarshallers (ex: Javascript's) will unmarshal JSON numbers to IEEE 754 +// double-precision floating point numbers, which means you can potentially +// silently lose precision. +var MarshalJSONWithoutQuotes = false + +// ExpMaxIterations specifies the maximum number of iterations needed to calculate +// precise natural exponent value using ExpHullAbrham method. +var ExpMaxIterations = 1000 + +// Zero constant, to make computations faster. +// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead. +var Zero = New(0, 1) + +var zeroInt = big.NewInt(0) +var oneInt = big.NewInt(1) +var twoInt = big.NewInt(2) +var fourInt = big.NewInt(4) +var fiveInt = big.NewInt(5) +var tenInt = big.NewInt(10) +var twentyInt = big.NewInt(20) + +var factorials = []Decimal{New(1, 0)} + +// Decimal represents a fixed-point decimal. It is immutable. +// number = value * 10 ^ exp +type Decimal struct { + value *big.Int + + // NOTE(vadim): this must be an int32, because we cast it to float64 during + // calculations. If exp is 64 bit, we might lose precision. + // If we cared about being able to represent every possible decimal, we + // could make exp a *big.Int but it would hurt performance and numbers + // like that are unrealistic. + exp int32 +} + +// New returns a new fixed-point decimal, value * 10 ^ exp. +func New(value int64, exp int32) Decimal { + return Decimal{ + value: big.NewInt(value), + exp: exp, + } +} + +// NewFromInt converts a int64 to Decimal. +// +// Example: +// +// NewFromInt(123).String() // output: "123" +// NewFromInt(-10).String() // output: "-10" +func NewFromInt(value int64) Decimal { + return Decimal{ + value: big.NewInt(value), + exp: 0, + } +} + +// NewFromInt32 converts a int32 to Decimal. +// +// Example: +// +// NewFromInt(123).String() // output: "123" +// NewFromInt(-10).String() // output: "-10" +func NewFromInt32(value int32) Decimal { + return Decimal{ + value: big.NewInt(int64(value)), + exp: 0, + } +} + +// NewFromBigInt returns a new Decimal from a big.Int, value * 10 ^ exp +func NewFromBigInt(value *big.Int, exp int32) Decimal { + return Decimal{ + value: new(big.Int).Set(value), + exp: exp, + } +} + +// NewFromString returns a new Decimal from a string representation. +// Trailing zeroes are not trimmed. +// +// Example: +// +// d, err := NewFromString("-123.45") +// d2, err := NewFromString(".0001") +// d3, err := NewFromString("1.47000") +// +func NewFromString(value string) (Decimal, error) { + originalInput := value + var intString string + var exp int64 + + // Check if number is using scientific notation + eIndex := strings.IndexAny(value, "Ee") + if eIndex != -1 { + expInt, err := strconv.ParseInt(value[eIndex+1:], 10, 32) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", value) + } + return Decimal{}, fmt.Errorf("can't convert %s to decimal: exponent is not numeric", value) + } + value = value[:eIndex] + exp = expInt + } + + pIndex := -1 + vLen := len(value) + for i := 0; i < vLen; i++ { + if value[i] == '.' { + if pIndex > -1 { + return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value) + } + pIndex = i + } + } + + if pIndex == -1 { + // There is no decimal point, we can just parse the original string as + // an int + intString = value + } else { + if pIndex+1 < vLen { + intString = value[:pIndex] + value[pIndex+1:] + } else { + intString = value[:pIndex] + } + expInt := -len(value[pIndex+1:]) + exp += int64(expInt) + } + + var dValue *big.Int + // strconv.ParseInt is faster than new(big.Int).SetString so this is just a shortcut for strings we know won't overflow + if len(intString) <= 18 { + parsed64, err := strconv.ParseInt(intString, 10, 64) + if err != nil { + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) + } + dValue = big.NewInt(parsed64) + } else { + dValue = new(big.Int) + _, ok := dValue.SetString(intString, 10) + if !ok { + return Decimal{}, fmt.Errorf("can't convert %s to decimal", value) + } + } + + if exp < math.MinInt32 || exp > math.MaxInt32 { + // NOTE(vadim): I doubt a string could realistically be this long + return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", originalInput) + } + + return Decimal{ + value: dValue, + exp: int32(exp), + }, nil +} + +// NewFromFormattedString returns a new Decimal from a formatted string representation. +// The second argument - replRegexp, is a regular expression that is used to find characters that should be +// removed from given decimal string representation. All matched characters will be replaced with an empty string. +// +// Example: +// +// r := regexp.MustCompile("[$,]") +// d1, err := NewFromFormattedString("$5,125.99", r) +// +// r2 := regexp.MustCompile("[_]") +// d2, err := NewFromFormattedString("1_000_000", r2) +// +// r3 := regexp.MustCompile("[USD\\s]") +// d3, err := NewFromFormattedString("5000 USD", r3) +// +func NewFromFormattedString(value string, replRegexp *regexp.Regexp) (Decimal, error) { + parsedValue := replRegexp.ReplaceAllString(value, "") + d, err := NewFromString(parsedValue) + if err != nil { + return Decimal{}, err + } + return d, nil +} + +// RequireFromString returns a new Decimal from a string representation +// or panics if NewFromString would have returned an error. +// +// Example: +// +// d := RequireFromString("-123.45") +// d2 := RequireFromString(".0001") +// +func RequireFromString(value string) Decimal { + dec, err := NewFromString(value) + if err != nil { + panic(err) + } + return dec +} + +// NewFromFloat converts a float64 to Decimal. +// +// The converted number will contain the number of significant digits that can be +// represented in a float with reliable roundtrip. +// This is typically 15 digits, but may be more in some cases. +// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. +// +// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. +// +// NOTE: this will panic on NaN, +/-inf +func NewFromFloat(value float64) Decimal { + if value == 0 { + return New(0, 0) + } + return newFromFloat(value, math.Float64bits(value), &float64info) +} + +// NewFromFloat32 converts a float32 to Decimal. +// +// The converted number will contain the number of significant digits that can be +// represented in a float with reliable roundtrip. +// This is typically 6-8 digits depending on the input. +// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information. +// +// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms. +// +// NOTE: this will panic on NaN, +/-inf +func NewFromFloat32(value float32) Decimal { + if value == 0 { + return New(0, 0) + } + // XOR is workaround for https://github.com/golang/go/issues/26285 + a := math.Float32bits(value) ^ 0x80808080 + return newFromFloat(float64(value), uint64(a)^0x80808080, &float32info) +} + +func newFromFloat(val float64, bits uint64, flt *floatInfo) Decimal { + if math.IsNaN(val) || math.IsInf(val, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", val)) + } + exp := int(bits>>flt.mantbits) & (1<>(flt.expbits+flt.mantbits) != 0 + + roundShortest(&d, mant, exp, flt) + // If less than 19 digits, we can do calculation in an int64. + if d.nd < 19 { + tmp := int64(0) + m := int64(1) + for i := d.nd - 1; i >= 0; i-- { + tmp += m * int64(d.d[i]-'0') + m *= 10 + } + if d.neg { + tmp *= -1 + } + return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)} + } + dValue := new(big.Int) + dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10) + if ok { + return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)} + } + + return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd)) +} + +// NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary +// number of fractional digits. +// +// Example: +// +// NewFromFloatWithExponent(123.456, -2).String() // output: "123.46" +// +func NewFromFloatWithExponent(value float64, exp int32) Decimal { + if math.IsNaN(value) || math.IsInf(value, 0) { + panic(fmt.Sprintf("Cannot create a Decimal from %v", value)) + } + + bits := math.Float64bits(value) + mant := bits & (1<<52 - 1) + exp2 := int32((bits >> 52) & (1<<11 - 1)) + sign := bits >> 63 + + if exp2 == 0 { + // specials + if mant == 0 { + return Decimal{} + } + // subnormal + exp2++ + } else { + // normal + mant |= 1 << 52 + } + + exp2 -= 1023 + 52 + + // normalizing base-2 values + for mant&1 == 0 { + mant = mant >> 1 + exp2++ + } + + // maximum number of fractional base-10 digits to represent 2^N exactly cannot be more than -N if N<0 + if exp < 0 && exp < exp2 { + if exp2 < 0 { + exp = exp2 + } else { + exp = 0 + } + } + + // representing 10^M * 2^N as 5^M * 2^(M+N) + exp2 -= exp + + temp := big.NewInt(1) + dMant := big.NewInt(int64(mant)) + + // applying 5^M + if exp > 0 { + temp = temp.SetInt64(int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + } else if exp < 0 { + temp = temp.SetInt64(-int64(exp)) + temp = temp.Exp(fiveInt, temp, nil) + dMant = dMant.Mul(dMant, temp) + temp = temp.SetUint64(1) + } + + // applying 2^(M+N) + if exp2 > 0 { + dMant = dMant.Lsh(dMant, uint(exp2)) + } else if exp2 < 0 { + temp = temp.Lsh(temp, uint(-exp2)) + } + + // rounding and downscaling + if exp > 0 || exp2 < 0 { + halfDown := new(big.Int).Rsh(temp, 1) + dMant = dMant.Add(dMant, halfDown) + dMant = dMant.Quo(dMant, temp) + } + + if sign == 1 { + dMant = dMant.Neg(dMant) + } + + return Decimal{ + value: dMant, + exp: exp, + } +} + +// Copy returns a copy of decimal with the same value and exponent, but a different pointer to value. +func (d Decimal) Copy() Decimal { + d.ensureInitialized() + return Decimal{ + value: &(*d.value), + exp: d.exp, + } +} + +// rescale returns a rescaled version of the decimal. Returned +// decimal may be less precise if the given exponent is bigger +// than the initial exponent of the Decimal. +// NOTE: this will truncate, NOT round +// +// Example: +// +// d := New(12345, -4) +// d2 := d.rescale(-1) +// d3 := d2.rescale(-4) +// println(d1) +// println(d2) +// println(d3) +// +// Output: +// +// 1.2345 +// 1.2 +// 1.2000 +// +func (d Decimal) rescale(exp int32) Decimal { + d.ensureInitialized() + + if d.exp == exp { + return Decimal{ + new(big.Int).Set(d.value), + d.exp, + } + } + + // NOTE(vadim): must convert exps to float64 before - to prevent overflow + diff := math.Abs(float64(exp) - float64(d.exp)) + value := new(big.Int).Set(d.value) + + expScale := new(big.Int).Exp(tenInt, big.NewInt(int64(diff)), nil) + if exp > d.exp { + value = value.Quo(value, expScale) + } else if exp < d.exp { + value = value.Mul(value, expScale) + } + + return Decimal{ + value: value, + exp: exp, + } +} + +// Abs returns the absolute value of the decimal. +func (d Decimal) Abs() Decimal { + if !d.IsNegative() { + return d + } + d.ensureInitialized() + d2Value := new(big.Int).Abs(d.value) + return Decimal{ + value: d2Value, + exp: d.exp, + } +} + +// Add returns d + d2. +func (d Decimal) Add(d2 Decimal) Decimal { + rd, rd2 := RescalePair(d, d2) + + d3Value := new(big.Int).Add(rd.value, rd2.value) + return Decimal{ + value: d3Value, + exp: rd.exp, + } +} + +// Sub returns d - d2. +func (d Decimal) Sub(d2 Decimal) Decimal { + rd, rd2 := RescalePair(d, d2) + + d3Value := new(big.Int).Sub(rd.value, rd2.value) + return Decimal{ + value: d3Value, + exp: rd.exp, + } +} + +// Neg returns -d. +func (d Decimal) Neg() Decimal { + d.ensureInitialized() + val := new(big.Int).Neg(d.value) + return Decimal{ + value: val, + exp: d.exp, + } +} + +// Mul returns d * d2. +func (d Decimal) Mul(d2 Decimal) Decimal { + d.ensureInitialized() + d2.ensureInitialized() + + expInt64 := int64(d.exp) + int64(d2.exp) + if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 { + // NOTE(vadim): better to panic than give incorrect results, as + // Decimals are usually used for money + panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64)) + } + + d3Value := new(big.Int).Mul(d.value, d2.value) + return Decimal{ + value: d3Value, + exp: int32(expInt64), + } +} + +// Shift shifts the decimal in base 10. +// It shifts left when shift is positive and right if shift is negative. +// In simpler terms, the given value for shift is added to the exponent +// of the decimal. +func (d Decimal) Shift(shift int32) Decimal { + d.ensureInitialized() + return Decimal{ + value: new(big.Int).Set(d.value), + exp: d.exp + shift, + } +} + +// Div returns d / d2. If it doesn't divide exactly, the result will have +// DivisionPrecision digits after the decimal point. +func (d Decimal) Div(d2 Decimal) Decimal { + return d.DivRound(d2, int32(DivisionPrecision)) +} + +// QuoRem does division with remainder +// d.QuoRem(d2,precision) returns quotient q and remainder r such that +// d = d2 * q + r, q an integer multiple of 10^(-precision) +// 0 <= r < abs(d2) * 10 ^(-precision) if d>=0 +// 0 >= r > -abs(d2) * 10 ^(-precision) if d<0 +// Note that precision<0 is allowed as input. +func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) { + d.ensureInitialized() + d2.ensureInitialized() + if d2.value.Sign() == 0 { + panic("decimal division by 0") + } + scale := -precision + e := int64(d.exp - d2.exp - scale) + if e > math.MaxInt32 || e < math.MinInt32 { + panic("overflow in decimal QuoRem") + } + var aa, bb, expo big.Int + var scalerest int32 + // d = a 10^ea + // d2 = b 10^eb + if e < 0 { + aa = *d.value + expo.SetInt64(-e) + bb.Exp(tenInt, &expo, nil) + bb.Mul(d2.value, &bb) + scalerest = d.exp + // now aa = a + // bb = b 10^(scale + eb - ea) + } else { + expo.SetInt64(e) + aa.Exp(tenInt, &expo, nil) + aa.Mul(d.value, &aa) + bb = *d2.value + scalerest = scale + d2.exp + // now aa = a ^ (ea - eb - scale) + // bb = b + } + var q, r big.Int + q.QuoRem(&aa, &bb, &r) + dq := Decimal{value: &q, exp: scale} + dr := Decimal{value: &r, exp: scalerest} + return dq, dr +} + +// DivRound divides and rounds to a given precision +// i.e. to an integer multiple of 10^(-precision) +// for a positive quotient digit 5 is rounded up, away from 0 +// if the quotient is negative then digit 5 is rounded down, away from 0 +// Note that precision<0 is allowed as input. +func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal { + // QuoRem already checks initialization + q, r := d.QuoRem(d2, precision) + // the actual rounding decision is based on comparing r*10^precision and d2/2 + // instead compare 2 r 10 ^precision and d2 + var rv2 big.Int + rv2.Abs(r.value) + rv2.Lsh(&rv2, 1) + // now rv2 = abs(r.value) * 2 + r2 := Decimal{value: &rv2, exp: r.exp + precision} + // r2 is now 2 * r * 10 ^ precision + var c = r2.Cmp(d2.Abs()) + + if c < 0 { + return q + } + + if d.value.Sign()*d2.value.Sign() < 0 { + return q.Sub(New(1, -precision)) + } + + return q.Add(New(1, -precision)) +} + +// Mod returns d % d2. +func (d Decimal) Mod(d2 Decimal) Decimal { + quo := d.DivRound(d2, -d.exp+1).Truncate(0) + return d.Sub(d2.Mul(quo)) +} + +// Pow returns d to the power d2 +func (d Decimal) Pow(d2 Decimal) Decimal { + var temp Decimal + if d2.IntPart() == 0 { + return NewFromFloat(1) + } + temp = d.Pow(d2.Div(NewFromFloat(2))) + if d2.IntPart()%2 == 0 { + return temp.Mul(temp) + } + if d2.IntPart() > 0 { + return temp.Mul(temp).Mul(d) + } + return temp.Mul(temp).Div(d) +} + +// ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm. +// OverallPrecision argument specifies the overall precision of the result (integer part + decimal part). +// +// ExpHullAbrham is faster than ExpTaylor for small precision values, but it is much slower for large precision values. +// +// Example: +// +// NewFromFloat(26.1).ExpHullAbrham(2).String() // output: "220000000000" +// NewFromFloat(26.1).ExpHullAbrham(20).String() // output: "216314672147.05767284" +// +func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) { + // Algorithm based on Variable precision exponential function. + // ACM Transactions on Mathematical Software by T. E. Hull & A. Abrham. + if d.IsZero() { + return Decimal{oneInt, 0}, nil + } + + currentPrecision := overallPrecision + + // Algorithm does not work if currentPrecision * 23 < |x|. + // Precision is automatically increased in such cases, so the value can be calculated precisely. + // If newly calculated precision is higher than ExpMaxIterations the currentPrecision will not be changed. + f := d.Abs().InexactFloat64() + if ncp := f / 23; ncp > float64(currentPrecision) && ncp < float64(ExpMaxIterations) { + currentPrecision = uint32(math.Ceil(ncp)) + } + + // fail if abs(d) beyond an over/underflow threshold + overflowThreshold := New(23*int64(currentPrecision), 0) + if d.Abs().Cmp(overflowThreshold) > 0 { + return Decimal{}, fmt.Errorf("over/underflow threshold, exp(x) cannot be calculated precisely") + } + + // Return 1 if abs(d) small enough; this also avoids later over/underflow + overflowThreshold2 := New(9, -int32(currentPrecision)-1) + if d.Abs().Cmp(overflowThreshold2) <= 0 { + return Decimal{oneInt, d.exp}, nil + } + + // t is the smallest integer >= 0 such that the corresponding abs(d/k) < 1 + t := d.exp + int32(d.NumDigits()) // Add d.NumDigits because the paper assumes that d.value [0.1, 1) + + if t < 0 { + t = 0 + } + + k := New(1, t) // reduction factor + r := Decimal{new(big.Int).Set(d.value), d.exp - t} // reduced argument + p := int32(currentPrecision) + t + 2 // precision for calculating the sum + + // Determine n, the number of therms for calculating sum + // use first Newton step (1.435p - 1.182) / log10(p/abs(r)) + // for solving appropriate equation, along with directed + // roundings and simple rational bound for log10(p/abs(r)) + rf := r.Abs().InexactFloat64() + pf := float64(p) + nf := math.Ceil((1.453*pf - 1.182) / math.Log10(pf/rf)) + if nf > float64(ExpMaxIterations) || math.IsNaN(nf) { + return Decimal{}, fmt.Errorf("exact value cannot be calculated in <=ExpMaxIterations iterations") + } + n := int64(nf) + + tmp := New(0, 0) + sum := New(1, 0) + one := New(1, 0) + for i := n - 1; i > 0; i-- { + tmp.value.SetInt64(i) + sum = sum.Mul(r.DivRound(tmp, p)) + sum = sum.Add(one) + } + + ki := k.IntPart() + res := New(1, 0) + for i := ki; i > 0; i-- { + res = res.Mul(sum) + } + + resNumDigits := int32(res.NumDigits()) + + var roundDigits int32 + if resNumDigits > abs(res.exp) { + roundDigits = int32(currentPrecision) - resNumDigits - res.exp + } else { + roundDigits = int32(currentPrecision) + } + + res = res.Round(roundDigits) + + return res, nil +} + +// ExpTaylor calculates the natural exponent of decimal (e to the power of d) using Taylor series expansion. +// Precision argument specifies how precise the result must be (number of digits after decimal point). +// Negative precision is allowed. +// +// ExpTaylor is much faster for large precision values than ExpHullAbrham. +// +// Example: +// +// d, err := NewFromFloat(26.1).ExpTaylor(2).String() +// d.String() // output: "216314672147.06" +// +// NewFromFloat(26.1).ExpTaylor(20).String() +// d.String() // output: "216314672147.05767284062928674083" +// +// NewFromFloat(26.1).ExpTaylor(-10).String() +// d.String() // output: "220000000000" +// +func (d Decimal) ExpTaylor(precision int32) (Decimal, error) { + // Note(mwoss): Implementation can be optimized by exclusively using big.Int API only + if d.IsZero() { + return Decimal{oneInt, 0}.Round(precision), nil + } + + var epsilon Decimal + var divPrecision int32 + if precision < 0 { + epsilon = New(1, -1) + divPrecision = 8 + } else { + epsilon = New(1, -precision-1) + divPrecision = precision + 1 + } + + decAbs := d.Abs() + pow := d.Abs() + factorial := New(1, 0) + + result := New(1, 0) + + for i := int64(1); ; { + step := pow.DivRound(factorial, divPrecision) + result = result.Add(step) + + // Stop Taylor series when current step is smaller than epsilon + if step.Cmp(epsilon) < 0 { + break + } + + pow = pow.Mul(decAbs) + + i++ + + // Calculate next factorial number or retrieve cached value + if len(factorials) >= int(i) && !factorials[i-1].IsZero() { + factorial = factorials[i-1] + } else { + // To avoid any race conditions, firstly the zero value is appended to a slice to create + // a spot for newly calculated factorial. After that, the zero value is replaced by calculated + // factorial using the index notation. + factorial = factorials[i-2].Mul(New(i, 0)) + factorials = append(factorials, Zero) + factorials[i-1] = factorial + } + } + + if d.Sign() < 0 { + result = New(1, 0).DivRound(result, precision+1) + } + + result = result.Round(precision) + return result, nil +} + +// NumDigits returns the number of digits of the decimal coefficient (d.Value) +// Note: Current implementation is extremely slow for large decimals and/or decimals with large fractional part +func (d Decimal) NumDigits() int { + // Note(mwoss): It can be optimized, unnecessary cast of big.Int to string + if d.IsNegative() { + return len(d.value.String()) - 1 + } + return len(d.value.String()) +} + +// IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false. +func (d Decimal) IsInteger() bool { + // The most typical case, all decimal with exponent higher or equal 0 can be represented as integer + if d.exp >= 0 { + return true + } + // When the exponent is negative we have to check every number after the decimal place + // If all of them are zeroes, we are sure that given decimal can be represented as an integer + var r big.Int + q := new(big.Int).Set(d.value) + for z := abs(d.exp); z > 0; z-- { + q.QuoRem(q, tenInt, &r) + if r.Cmp(zeroInt) != 0 { + return false + } + } + return true +} + +// Abs calculates absolute value of any int32. Used for calculating absolute value of decimal's exponent. +func abs(n int32) int32 { + if n < 0 { + return -n + } + return n +} + +// Cmp compares the numbers represented by d and d2 and returns: +// +// -1 if d < d2 +// 0 if d == d2 +// +1 if d > d2 +// +func (d Decimal) Cmp(d2 Decimal) int { + d.ensureInitialized() + d2.ensureInitialized() + + if d.exp == d2.exp { + return d.value.Cmp(d2.value) + } + + rd, rd2 := RescalePair(d, d2) + + return rd.value.Cmp(rd2.value) +} + +// Equal returns whether the numbers represented by d and d2 are equal. +func (d Decimal) Equal(d2 Decimal) bool { + return d.Cmp(d2) == 0 +} + +// Equals is deprecated, please use Equal method instead +func (d Decimal) Equals(d2 Decimal) bool { + return d.Equal(d2) +} + +// GreaterThan (GT) returns true when d is greater than d2. +func (d Decimal) GreaterThan(d2 Decimal) bool { + return d.Cmp(d2) == 1 +} + +// GreaterThanOrEqual (GTE) returns true when d is greater than or equal to d2. +func (d Decimal) GreaterThanOrEqual(d2 Decimal) bool { + cmp := d.Cmp(d2) + return cmp == 1 || cmp == 0 +} + +// LessThan (LT) returns true when d is less than d2. +func (d Decimal) LessThan(d2 Decimal) bool { + return d.Cmp(d2) == -1 +} + +// LessThanOrEqual (LTE) returns true when d is less than or equal to d2. +func (d Decimal) LessThanOrEqual(d2 Decimal) bool { + cmp := d.Cmp(d2) + return cmp == -1 || cmp == 0 +} + +// Sign returns: +// +// -1 if d < 0 +// 0 if d == 0 +// +1 if d > 0 +// +func (d Decimal) Sign() int { + if d.value == nil { + return 0 + } + return d.value.Sign() +} + +// IsPositive return +// +// true if d > 0 +// false if d == 0 +// false if d < 0 +func (d Decimal) IsPositive() bool { + return d.Sign() == 1 +} + +// IsNegative return +// +// true if d < 0 +// false if d == 0 +// false if d > 0 +func (d Decimal) IsNegative() bool { + return d.Sign() == -1 +} + +// IsZero return +// +// true if d == 0 +// false if d > 0 +// false if d < 0 +func (d Decimal) IsZero() bool { + return d.Sign() == 0 +} + +// Exponent returns the exponent, or scale component of the decimal. +func (d Decimal) Exponent() int32 { + return d.exp +} + +// Coefficient returns the coefficient of the decimal. It is scaled by 10^Exponent() +func (d Decimal) Coefficient() *big.Int { + d.ensureInitialized() + // we copy the coefficient so that mutating the result does not mutate the Decimal. + return new(big.Int).Set(d.value) +} + +// CoefficientInt64 returns the coefficient of the decimal as int64. It is scaled by 10^Exponent() +// If coefficient cannot be represented in an int64, the result will be undefined. +func (d Decimal) CoefficientInt64() int64 { + d.ensureInitialized() + return d.value.Int64() +} + +// IntPart returns the integer component of the decimal. +func (d Decimal) IntPart() int64 { + scaledD := d.rescale(0) + return scaledD.value.Int64() +} + +// BigInt returns integer component of the decimal as a BigInt. +func (d Decimal) BigInt() *big.Int { + scaledD := d.rescale(0) + i := &big.Int{} + i.SetString(scaledD.String(), 10) + return i +} + +// BigFloat returns decimal as BigFloat. +// Be aware that casting decimal to BigFloat might cause a loss of precision. +func (d Decimal) BigFloat() *big.Float { + f := &big.Float{} + f.SetString(d.String()) + return f +} + +// Rat returns a rational number representation of the decimal. +func (d Decimal) Rat() *big.Rat { + d.ensureInitialized() + if d.exp <= 0 { + // NOTE(vadim): must negate after casting to prevent int32 overflow + denom := new(big.Int).Exp(tenInt, big.NewInt(-int64(d.exp)), nil) + return new(big.Rat).SetFrac(d.value, denom) + } + + mul := new(big.Int).Exp(tenInt, big.NewInt(int64(d.exp)), nil) + num := new(big.Int).Mul(d.value, mul) + return new(big.Rat).SetFrac(num, oneInt) +} + +// Float64 returns the nearest float64 value for d and a bool indicating +// whether f represents d exactly. +// For more details, see the documentation for big.Rat.Float64 +func (d Decimal) Float64() (f float64, exact bool) { + return d.Rat().Float64() +} + +// InexactFloat64 returns the nearest float64 value for d. +// It doesn't indicate if the returned value represents d exactly. +func (d Decimal) InexactFloat64() float64 { + f, _ := d.Float64() + return f +} + +// String returns the string representation of the decimal +// with the fixed point. +// +// Example: +// +// d := New(-12345, -3) +// println(d.String()) +// +// Output: +// +// -12.345 +// +func (d Decimal) String() string { + return d.string(true) +} + +// StringFixed returns a rounded fixed-point string with places digits after +// the decimal point. +// +// Example: +// +// NewFromFloat(0).StringFixed(2) // output: "0.00" +// NewFromFloat(0).StringFixed(0) // output: "0" +// NewFromFloat(5.45).StringFixed(0) // output: "5" +// NewFromFloat(5.45).StringFixed(1) // output: "5.5" +// NewFromFloat(5.45).StringFixed(2) // output: "5.45" +// NewFromFloat(5.45).StringFixed(3) // output: "5.450" +// NewFromFloat(545).StringFixed(-1) // output: "550" +// +func (d Decimal) StringFixed(places int32) string { + rounded := d.Round(places) + return rounded.string(false) +} + +// StringFixedBank returns a banker rounded fixed-point string with places digits +// after the decimal point. +// +// Example: +// +// NewFromFloat(0).StringFixedBank(2) // output: "0.00" +// NewFromFloat(0).StringFixedBank(0) // output: "0" +// NewFromFloat(5.45).StringFixedBank(0) // output: "5" +// NewFromFloat(5.45).StringFixedBank(1) // output: "5.4" +// NewFromFloat(5.45).StringFixedBank(2) // output: "5.45" +// NewFromFloat(5.45).StringFixedBank(3) // output: "5.450" +// NewFromFloat(545).StringFixedBank(-1) // output: "540" +// +func (d Decimal) StringFixedBank(places int32) string { + rounded := d.RoundBank(places) + return rounded.string(false) +} + +// StringFixedCash returns a Swedish/Cash rounded fixed-point string. For +// more details see the documentation at function RoundCash. +func (d Decimal) StringFixedCash(interval uint8) string { + rounded := d.RoundCash(interval) + return rounded.string(false) +} + +// Round rounds the decimal to places decimal places. +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Example: +// +// NewFromFloat(5.45).Round(1).String() // output: "5.5" +// NewFromFloat(545).Round(-1).String() // output: "550" +// +func (d Decimal) Round(places int32) Decimal { + if d.exp == -places { + return d + } + // truncate to places + 1 + ret := d.rescale(-places - 1) + + // add sign(d) * 0.5 + if ret.value.Sign() < 0 { + ret.value.Sub(ret.value, fiveInt) + } else { + ret.value.Add(ret.value, fiveInt) + } + + // floor for positive numbers, ceil for negative numbers + _, m := ret.value.DivMod(ret.value, tenInt, new(big.Int)) + ret.exp++ + if ret.value.Sign() < 0 && m.Cmp(zeroInt) != 0 { + ret.value.Add(ret.value, oneInt) + } + + return ret +} + +// RoundCeil rounds the decimal towards +infinity. +// +// Example: +// +// NewFromFloat(545).RoundCeil(-2).String() // output: "600" +// NewFromFloat(500).RoundCeil(-2).String() // output: "500" +// NewFromFloat(1.1001).RoundCeil(2).String() // output: "1.11" +// NewFromFloat(-1.454).RoundCeil(1).String() // output: "-1.5" +// +func (d Decimal) RoundCeil(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.value.Sign() > 0 { + rescaled.value.Add(rescaled.value, oneInt) + } + + return rescaled +} + +// RoundFloor rounds the decimal towards -infinity. +// +// Example: +// +// NewFromFloat(545).RoundFloor(-2).String() // output: "500" +// NewFromFloat(-500).RoundFloor(-2).String() // output: "-500" +// NewFromFloat(1.1001).RoundFloor(2).String() // output: "1.1" +// NewFromFloat(-1.454).RoundFloor(1).String() // output: "-1.4" +// +func (d Decimal) RoundFloor(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.value.Sign() < 0 { + rescaled.value.Sub(rescaled.value, oneInt) + } + + return rescaled +} + +// RoundUp rounds the decimal away from zero. +// +// Example: +// +// NewFromFloat(545).RoundUp(-2).String() // output: "600" +// NewFromFloat(500).RoundUp(-2).String() // output: "500" +// NewFromFloat(1.1001).RoundUp(2).String() // output: "1.11" +// NewFromFloat(-1.454).RoundUp(1).String() // output: "-1.4" +// +func (d Decimal) RoundUp(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + + if d.value.Sign() > 0 { + rescaled.value.Add(rescaled.value, oneInt) + } else if d.value.Sign() < 0 { + rescaled.value.Sub(rescaled.value, oneInt) + } + + return rescaled +} + +// RoundDown rounds the decimal towards zero. +// +// Example: +// +// NewFromFloat(545).RoundDown(-2).String() // output: "500" +// NewFromFloat(-500).RoundDown(-2).String() // output: "-500" +// NewFromFloat(1.1001).RoundDown(2).String() // output: "1.1" +// NewFromFloat(-1.454).RoundDown(1).String() // output: "-1.5" +// +func (d Decimal) RoundDown(places int32) Decimal { + if d.exp >= -places { + return d + } + + rescaled := d.rescale(-places) + if d.Equal(rescaled) { + return d + } + return rescaled +} + +// RoundBank rounds the decimal to places decimal places. +// If the final digit to round is equidistant from the nearest two integers the +// rounded value is taken as the even number +// +// If places < 0, it will round the integer part to the nearest 10^(-places). +// +// Examples: +// +// NewFromFloat(5.45).RoundBank(1).String() // output: "5.4" +// NewFromFloat(545).RoundBank(-1).String() // output: "540" +// NewFromFloat(5.46).RoundBank(1).String() // output: "5.5" +// NewFromFloat(546).RoundBank(-1).String() // output: "550" +// NewFromFloat(5.55).RoundBank(1).String() // output: "5.6" +// NewFromFloat(555).RoundBank(-1).String() // output: "560" +// +func (d Decimal) RoundBank(places int32) Decimal { + + round := d.Round(places) + remainder := d.Sub(round).Abs() + + half := New(5, -places-1) + if remainder.Cmp(half) == 0 && round.value.Bit(0) != 0 { + if round.value.Sign() < 0 { + round.value.Add(round.value, oneInt) + } else { + round.value.Sub(round.value, oneInt) + } + } + + return round +} + +// RoundCash aka Cash/Penny/öre rounding rounds decimal to a specific +// interval. The amount payable for a cash transaction is rounded to the nearest +// multiple of the minimum currency unit available. The following intervals are +// available: 5, 10, 25, 50 and 100; any other number throws a panic. +// 5: 5 cent rounding 3.43 => 3.45 +// 10: 10 cent rounding 3.45 => 3.50 (5 gets rounded up) +// 25: 25 cent rounding 3.41 => 3.50 +// 50: 50 cent rounding 3.75 => 4.00 +// 100: 100 cent rounding 3.50 => 4.00 +// For more details: https://en.wikipedia.org/wiki/Cash_rounding +func (d Decimal) RoundCash(interval uint8) Decimal { + var iVal *big.Int + switch interval { + case 5: + iVal = twentyInt + case 10: + iVal = tenInt + case 25: + iVal = fourInt + case 50: + iVal = twoInt + case 100: + iVal = oneInt + default: + panic(fmt.Sprintf("Decimal does not support this Cash rounding interval `%d`. Supported: 5, 10, 25, 50, 100", interval)) + } + dVal := Decimal{ + value: iVal, + } + + // TODO: optimize those calculations to reduce the high allocations (~29 allocs). + return d.Mul(dVal).Round(0).Div(dVal).Truncate(2) +} + +// Floor returns the nearest integer value less than or equal to d. +func (d Decimal) Floor() Decimal { + d.ensureInitialized() + + if d.exp >= 0 { + return d + } + + exp := big.NewInt(10) + + // NOTE(vadim): must negate after casting to prevent int32 overflow + exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) + + z := new(big.Int).Div(d.value, exp) + return Decimal{value: z, exp: 0} +} + +// Ceil returns the nearest integer value greater than or equal to d. +func (d Decimal) Ceil() Decimal { + d.ensureInitialized() + + if d.exp >= 0 { + return d + } + + exp := big.NewInt(10) + + // NOTE(vadim): must negate after casting to prevent int32 overflow + exp.Exp(exp, big.NewInt(-int64(d.exp)), nil) + + z, m := new(big.Int).DivMod(d.value, exp, new(big.Int)) + if m.Cmp(zeroInt) != 0 { + z.Add(z, oneInt) + } + return Decimal{value: z, exp: 0} +} + +// Truncate truncates off digits from the number, without rounding. +// +// NOTE: precision is the last digit that will not be truncated (must be >= 0). +// +// Example: +// +// decimal.NewFromString("123.456").Truncate(2).String() // "123.45" +// +func (d Decimal) Truncate(precision int32) Decimal { + d.ensureInitialized() + if precision >= 0 && -precision > d.exp { + return d.rescale(-precision) + } + return d +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + return nil + } + + str, err := unquoteIfQuoted(decimalBytes) + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", decimalBytes, err) + } + + decimal, err := NewFromString(str) + *d = decimal + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", str, err) + } + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (d Decimal) MarshalJSON() ([]byte, error) { + var str string + if MarshalJSONWithoutQuotes { + str = d.String() + } else { + str = "\"" + d.String() + "\"" + } + return []byte(str), nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation +// is already used when encoding to text, this method stores that string as []byte +func (d *Decimal) UnmarshalBinary(data []byte) error { + // Verify we have at least 4 bytes for the exponent. The GOB encoded value + // may be empty. + if len(data) < 4 { + return fmt.Errorf("error decoding binary %v: expected at least 4 bytes, got %d", data, len(data)) + } + + // Extract the exponent + d.exp = int32(binary.BigEndian.Uint32(data[:4])) + + // Extract the value + d.value = new(big.Int) + if err := d.value.GobDecode(data[4:]); err != nil { + return fmt.Errorf("error decoding binary %v: %s", data, err) + } + + return nil +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Decimal) MarshalBinary() (data []byte, err error) { + // Write the exponent first since it's a fixed size + v1 := make([]byte, 4) + binary.BigEndian.PutUint32(v1, uint32(d.exp)) + + // Add the value + var v2 []byte + if v2, err = d.value.GobEncode(); err != nil { + return + } + + // Return the byte array + data = append(v1, v2...) + return +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *Decimal) Scan(value interface{}) error { + // first try to see if the data is stored in database as a Numeric datatype + switch v := value.(type) { + + case float32: + *d = NewFromFloat(float64(v)) + return nil + + case float64: + // numeric in sqlite3 sends us float64 + *d = NewFromFloat(v) + return nil + + case int64: + // at least in sqlite3 when the value is 0 in db, the data is sent + // to us as an int64 instead of a float64 ... + *d = New(v, 0) + return nil + + default: + // default is trying to interpret value stored as string + str, err := unquoteIfQuoted(v) + if err != nil { + return err + } + *d, err = NewFromString(str) + return err + } +} + +// Value implements the driver.Valuer interface for database serialization. +func (d Decimal) Value() (driver.Value, error) { + return d.String(), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization. +func (d *Decimal) UnmarshalText(text []byte) error { + str := string(text) + + dec, err := NewFromString(str) + *d = dec + if err != nil { + return fmt.Errorf("error decoding string '%s': %s", str, err) + } + + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d Decimal) MarshalText() (text []byte, err error) { + return []byte(d.String()), nil +} + +// GobEncode implements the gob.GobEncoder interface for gob serialization. +func (d Decimal) GobEncode() ([]byte, error) { + return d.MarshalBinary() +} + +// GobDecode implements the gob.GobDecoder interface for gob serialization. +func (d *Decimal) GobDecode(data []byte) error { + return d.UnmarshalBinary(data) +} + +// StringScaled first scales the decimal then calls .String() on it. +// NOTE: buggy, unintuitive, and DEPRECATED! Use StringFixed instead. +func (d Decimal) StringScaled(exp int32) string { + return d.rescale(exp).String() +} + +func (d Decimal) string(trimTrailingZeros bool) string { + if d.exp >= 0 { + return d.rescale(0).value.String() + } + + abs := new(big.Int).Abs(d.value) + str := abs.String() + + var intPart, fractionalPart string + + // NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN + // and you are on a 32-bit machine. Won't fix this super-edge case. + dExpInt := int(d.exp) + if len(str) > -dExpInt { + intPart = str[:len(str)+dExpInt] + fractionalPart = str[len(str)+dExpInt:] + } else { + intPart = "0" + + num0s := -dExpInt - len(str) + fractionalPart = strings.Repeat("0", num0s) + str + } + + if trimTrailingZeros { + i := len(fractionalPart) - 1 + for ; i >= 0; i-- { + if fractionalPart[i] != '0' { + break + } + } + fractionalPart = fractionalPart[:i+1] + } + + number := intPart + if len(fractionalPart) > 0 { + number += "." + fractionalPart + } + + if d.value.Sign() < 0 { + return "-" + number + } + + return number +} + +func (d *Decimal) ensureInitialized() { + if d.value == nil { + d.value = new(big.Int) + } +} + +// Min returns the smallest Decimal that was passed in the arguments. +// +// To call this function with an array, you must do: +// +// Min(arr[0], arr[1:]...) +// +// This makes it harder to accidentally call Min with 0 arguments. +func Min(first Decimal, rest ...Decimal) Decimal { + ans := first + for _, item := range rest { + if item.Cmp(ans) < 0 { + ans = item + } + } + return ans +} + +// Max returns the largest Decimal that was passed in the arguments. +// +// To call this function with an array, you must do: +// +// Max(arr[0], arr[1:]...) +// +// This makes it harder to accidentally call Max with 0 arguments. +func Max(first Decimal, rest ...Decimal) Decimal { + ans := first + for _, item := range rest { + if item.Cmp(ans) > 0 { + ans = item + } + } + return ans +} + +// Sum returns the combined total of the provided first and rest Decimals +func Sum(first Decimal, rest ...Decimal) Decimal { + total := first + for _, item := range rest { + total = total.Add(item) + } + + return total +} + +// Avg returns the average value of the provided first and rest Decimals +func Avg(first Decimal, rest ...Decimal) Decimal { + count := New(int64(len(rest)+1), 0) + sum := Sum(first, rest...) + return sum.Div(count) +} + +// RescalePair rescales two decimals to common exponential value (minimal exp of both decimals) +func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) { + d1.ensureInitialized() + d2.ensureInitialized() + + if d1.exp == d2.exp { + return d1, d2 + } + + baseScale := min(d1.exp, d2.exp) + if baseScale != d1.exp { + return d1.rescale(baseScale), d2 + } + return d1, d2.rescale(baseScale) +} + +func min(x, y int32) int32 { + if x >= y { + return y + } + return x +} + +func unquoteIfQuoted(value interface{}) (string, error) { + var bytes []byte + + switch v := value.(type) { + case string: + bytes = []byte(v) + case []byte: + bytes = v + default: + return "", fmt.Errorf("could not convert value '%+v' to byte array of type '%T'", + value, value) + } + + // If the amount is quoted, strip the quotes + if len(bytes) > 2 && bytes[0] == '"' && bytes[len(bytes)-1] == '"' { + bytes = bytes[1 : len(bytes)-1] + } + return string(bytes), nil +} + +// NullDecimal represents a nullable decimal with compatibility for +// scanning null values from the database. +type NullDecimal struct { + Decimal Decimal + Valid bool +} + +func NewNullDecimal(d Decimal) NullDecimal { + return NullDecimal{ + Decimal: d, + Valid: true, + } +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (d *NullDecimal) Scan(value interface{}) error { + if value == nil { + d.Valid = false + return nil + } + d.Valid = true + return d.Decimal.Scan(value) +} + +// Value implements the driver.Valuer interface for database serialization. +func (d NullDecimal) Value() (driver.Value, error) { + if !d.Valid { + return nil, nil + } + return d.Decimal.Value() +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error { + if string(decimalBytes) == "null" { + d.Valid = false + return nil + } + d.Valid = true + return d.Decimal.UnmarshalJSON(decimalBytes) +} + +// MarshalJSON implements the json.Marshaler interface. +func (d NullDecimal) MarshalJSON() ([]byte, error) { + if !d.Valid { + return []byte("null"), nil + } + return d.Decimal.MarshalJSON() +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for XML +// deserialization +func (d *NullDecimal) UnmarshalText(text []byte) error { + str := string(text) + + // check for empty XML or XML without body e.g., + if str == "" { + d.Valid = false + return nil + } + if err := d.Decimal.UnmarshalText(text); err != nil { + d.Valid = false + return err + } + d.Valid = true + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface for XML +// serialization. +func (d NullDecimal) MarshalText() (text []byte, err error) { + if !d.Valid { + return []byte{}, nil + } + return d.Decimal.MarshalText() +} + +// Trig functions + +// Atan returns the arctangent, in radians, of x. +func (d Decimal) Atan() Decimal { + if d.Equal(NewFromFloat(0.0)) { + return d + } + if d.GreaterThan(NewFromFloat(0.0)) { + return d.satan() + } + return d.Neg().satan().Neg() +} + +func (d Decimal) xatan() Decimal { + P0 := NewFromFloat(-8.750608600031904122785e-01) + P1 := NewFromFloat(-1.615753718733365076637e+01) + P2 := NewFromFloat(-7.500855792314704667340e+01) + P3 := NewFromFloat(-1.228866684490136173410e+02) + P4 := NewFromFloat(-6.485021904942025371773e+01) + Q0 := NewFromFloat(2.485846490142306297962e+01) + Q1 := NewFromFloat(1.650270098316988542046e+02) + Q2 := NewFromFloat(4.328810604912902668951e+02) + Q3 := NewFromFloat(4.853903996359136964868e+02) + Q4 := NewFromFloat(1.945506571482613964425e+02) + z := d.Mul(d) + b1 := P0.Mul(z).Add(P1).Mul(z).Add(P2).Mul(z).Add(P3).Mul(z).Add(P4).Mul(z) + b2 := z.Add(Q0).Mul(z).Add(Q1).Mul(z).Add(Q2).Mul(z).Add(Q3).Mul(z).Add(Q4) + z = b1.Div(b2) + z = d.Mul(z).Add(d) + return z +} + +// satan reduces its argument (known to be positive) +// to the range [0, 0.66] and calls xatan. +func (d Decimal) satan() Decimal { + Morebits := NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits + Tan3pio8 := NewFromFloat(2.41421356237309504880) // tan(3*pi/8) + pi := NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459) + + if d.LessThanOrEqual(NewFromFloat(0.66)) { + return d.xatan() + } + if d.GreaterThan(Tan3pio8) { + return pi.Div(NewFromFloat(2.0)).Sub(NewFromFloat(1.0).Div(d).xatan()).Add(Morebits) + } + return pi.Div(NewFromFloat(4.0)).Add((d.Sub(NewFromFloat(1.0)).Div(d.Add(NewFromFloat(1.0)))).xatan()).Add(NewFromFloat(0.5).Mul(Morebits)) +} + +// sin coefficients +var _sin = [...]Decimal{ + NewFromFloat(1.58962301576546568060e-10), // 0x3de5d8fd1fd19ccd + NewFromFloat(-2.50507477628578072866e-8), // 0xbe5ae5e5a9291f5d + NewFromFloat(2.75573136213857245213e-6), // 0x3ec71de3567d48a1 + NewFromFloat(-1.98412698295895385996e-4), // 0xbf2a01a019bfdf03 + NewFromFloat(8.33333333332211858878e-3), // 0x3f8111111110f7d0 + NewFromFloat(-1.66666666666666307295e-1), // 0xbfc5555555555548 +} + +// Sin returns the sine of the radian argument x. +func (d Decimal) Sin() Decimal { + PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts + PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, + PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, + M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + if d.Equal(NewFromFloat(0.0)) { + return d + } + // make argument positive but save the sign + sign := false + if d.LessThan(NewFromFloat(0.0)) { + d = d.Neg() + sign = true + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(NewFromFloat(1.0)) + } + j &= 7 // octant modulo 2Pi radians (360 degrees) + // reflect in x axis + if j > 3 { + sign = !sign + j -= 4 + } + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if j == 1 || j == 2 { + w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) + y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w) + } else { + y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) + } + if sign { + y = y.Neg() + } + return y +} + +// cos coefficients +var _cos = [...]Decimal{ + NewFromFloat(-1.13585365213876817300e-11), // 0xbda8fa49a0861a9b + NewFromFloat(2.08757008419747316778e-9), // 0x3e21ee9d7b4e3f05 + NewFromFloat(-2.75573141792967388112e-7), // 0xbe927e4f7eac4bc6 + NewFromFloat(2.48015872888517045348e-5), // 0x3efa01a019c844f5 + NewFromFloat(-1.38888888888730564116e-3), // 0xbf56c16c16c14f91 + NewFromFloat(4.16666666666665929218e-2), // 0x3fa555555555554b +} + +// Cos returns the cosine of the radian argument x. +func (d Decimal) Cos() Decimal { + + PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts + PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, + PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, + M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + // make argument positive + sign := false + if d.LessThan(NewFromFloat(0.0)) { + d = d.Neg() + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(NewFromFloat(1.0)) + } + j &= 7 // octant modulo 2Pi radians (360 degrees) + // reflect in x axis + if j > 3 { + sign = !sign + j -= 4 + } + if j > 1 { + sign = !sign + } + + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if j == 1 || j == 2 { + y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5]))) + } else { + w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5])) + y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w) + } + if sign { + y = y.Neg() + } + return y +} + +var _tanP = [...]Decimal{ + NewFromFloat(-1.30936939181383777646e+4), // 0xc0c992d8d24f3f38 + NewFromFloat(1.15351664838587416140e+6), // 0x413199eca5fc9ddd + NewFromFloat(-1.79565251976484877988e+7), // 0xc1711fead3299176 +} +var _tanQ = [...]Decimal{ + NewFromFloat(1.00000000000000000000e+0), + NewFromFloat(1.36812963470692954678e+4), //0x40cab8a5eeb36572 + NewFromFloat(-1.32089234440210967447e+6), //0xc13427bc582abc96 + NewFromFloat(2.50083801823357915839e+7), //0x4177d98fc2ead8ef + NewFromFloat(-5.38695755929454629881e+7), //0xc189afe03cbe5a31 +} + +// Tan returns the tangent of the radian argument x. +func (d Decimal) Tan() Decimal { + + PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts + PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000, + PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170, + M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi + + if d.Equal(NewFromFloat(0.0)) { + return d + } + + // make argument positive but save the sign + sign := false + if d.LessThan(NewFromFloat(0.0)) { + d = d.Neg() + sign = true + } + + j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle + y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float + + // map zeros to origin + if j&1 == 1 { + j++ + y = y.Add(NewFromFloat(1.0)) + } + + z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic + zz := z.Mul(z) + + if zz.GreaterThan(NewFromFloat(1e-14)) { + w := zz.Mul(_tanP[0].Mul(zz).Add(_tanP[1]).Mul(zz).Add(_tanP[2])) + x := zz.Add(_tanQ[1]).Mul(zz).Add(_tanQ[2]).Mul(zz).Add(_tanQ[3]).Mul(zz).Add(_tanQ[4]) + y = z.Add(z.Mul(w.Div(x))) + } else { + y = z + } + if j&2 == 2 { + y = NewFromFloat(-1.0).Div(y) + } + if sign { + y = y.Neg() + } + return y +} diff --git a/vendor/gitee.com/chunanyong/zorm/decimal/rounding.go b/vendor/gitee.com/chunanyong/zorm/decimal/rounding.go new file mode 100644 index 00000000..d4b0cd00 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/decimal/rounding.go @@ -0,0 +1,160 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Multiprecision decimal numbers. +// For floating-point formatting only; not general purpose. +// Only operations are assign and (binary) left/right shift. +// Can do binary floating point in multiprecision decimal precisely +// because 2 divides 10; cannot do decimal floating point +// in multiprecision binary precisely. + +package decimal + +type floatInfo struct { + mantbits uint + expbits uint + bias int +} + +var float32info = floatInfo{23, 8, -127} +var float64info = floatInfo{52, 11, -1023} + +// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits +// that will let the original floating point value be precisely reconstructed. +func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) { + // If mantissa is zero, the number is zero; stop now. + if mant == 0 { + d.nd = 0 + return + } + + // Compute upper and lower such that any decimal number + // between upper and lower (possibly inclusive) + // will round to the original floating point number. + + // We may see at once that the number is already shortest. + // + // Suppose d is not denormal, so that 2^exp <= d < 10^dp. + // The closest shorter number is at least 10^(dp-nd) away. + // The lower/upper bounds computed below are at distance + // at most 2^(exp-mantbits). + // + // So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits), + // or equivalently log2(10)*(dp-nd) > exp-mantbits. + // It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32). + minexp := flt.bias + 1 // minimum possible exponent + if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) { + // The number is already shortest. + return + } + + // d = mant << (exp - mantbits) + // Next highest floating point number is mant+1 << exp-mantbits. + // Our upper bound is halfway between, mant*2+1 << exp-mantbits-1. + upper := new(decimal) + upper.Assign(mant*2 + 1) + upper.Shift(exp - int(flt.mantbits) - 1) + + // d = mant << (exp - mantbits) + // Next lowest floating point number is mant-1 << exp-mantbits, + // unless mant-1 drops the significant bit and exp is not the minimum exp, + // in which case the next lowest is mant*2-1 << exp-mantbits-1. + // Either way, call it mantlo << explo-mantbits. + // Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1. + var mantlo uint64 + var explo int + if mant > 1<= d.nd { + break + } + li := ui - upper.dp + lower.dp + l := byte('0') // lower digit + if li >= 0 && li < lower.nd { + l = lower.d[li] + } + m := byte('0') // middle digit + if mi >= 0 { + m = d.d[mi] + } + u := byte('0') // upper digit + if ui < upper.nd { + u = upper.d[ui] + } + + // Okay to round down (truncate) if lower has a different digit + // or if lower is inclusive and is exactly the result of rounding + // down (i.e., and we have reached the final digit of lower). + okdown := l != m || inclusive && li+1 == lower.nd + + switch { + case upperdelta == 0 && m+1 < u: + // Example: + // m = 12345xxx + // u = 12347xxx + upperdelta = 2 + case upperdelta == 0 && m != u: + // Example: + // m = 12345xxx + // u = 12346xxx + upperdelta = 1 + case upperdelta == 1 && (m != '9' || u != '0'): + // Example: + // m = 1234598x + // u = 1234600x + upperdelta = 2 + } + // Okay to round up if upper has a different digit and either upper + // is inclusive or upper is bigger than the result of rounding up. + okup := upperdelta > 0 && (inclusive || upperdelta > 1 || ui+1 < upper.nd) + + // If it's okay to do either, then round to the nearest one. + // If it's okay to do only one, do it. + switch { + case okdown && okup: + d.Round(mi + 1) + return + case okdown: + d.RoundDown(mi + 1) + return + case okup: + d.RoundUp(mi + 1) + return + } + } +} diff --git a/vendor/gitee.com/chunanyong/zorm/dialect.go b/vendor/gitee.com/chunanyong/zorm/dialect.go new file mode 100644 index 00000000..e6277514 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/dialect.go @@ -0,0 +1,1084 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "context" + "crypto/rand" + "database/sql" + "errors" + "fmt" + "math/big" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +// wrapPageSQL 包装分页的SQL语句 +// wrapPageSQL SQL statement for wrapping paging +func wrapPageSQL(dialect string, sqlstr *string, page *Page) error { + if page.PageNo < 1 { // 默认第一页 + page.PageNo = 1 + } + var sqlbuilder strings.Builder + sqlbuilder.Grow(stringBuilderGrowLen) + sqlbuilder.WriteString(*sqlstr) + switch dialect { + case "mysql", "sqlite", "dm", "gbase", "clickhouse", "tdengine", "db2": // MySQL,sqlite3,dm,南通,clickhouse,TDengine,db2 7.2+ + sqlbuilder.WriteString(" LIMIT ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize * (page.PageNo - 1))) + sqlbuilder.WriteByte(',') + sqlbuilder.WriteString(strconv.Itoa(page.PageSize)) + + case "postgresql", "kingbase", "shentong": // postgresql,kingbase,神通数据库 + sqlbuilder.WriteString(" LIMIT ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize)) + sqlbuilder.WriteString(" OFFSET ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize * (page.PageNo - 1))) + case "mssql": // sqlserver 2012+ + locOrderBy := findOrderByIndex(sqlstr) + if len(locOrderBy) < 1 { // 如果没有 order by,增加默认的排序 + sqlbuilder.WriteString(" ORDER BY (SELECT NULL) ") + } + sqlbuilder.WriteString(" OFFSET ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize * (page.PageNo - 1))) + sqlbuilder.WriteString(" ROWS FETCH NEXT ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize)) + sqlbuilder.WriteString(" ROWS ONLY ") + case "oracle": // oracle 12c+ + locOrderBy := findOrderByIndex(sqlstr) + if len(locOrderBy) < 1 { // 如果没有 order by,增加默认的排序 + sqlbuilder.WriteString(" ORDER BY NULL ") + } + sqlbuilder.WriteString(" OFFSET ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize * (page.PageNo - 1))) + sqlbuilder.WriteString(" ROWS FETCH NEXT ") + sqlbuilder.WriteString(strconv.Itoa(page.PageSize)) + sqlbuilder.WriteString(" ROWS ONLY ") + default: + return errors.New("->wrapPageSQL-->不支持的数据库类型:" + dialect) + + } + *sqlstr = sqlbuilder.String() + // return reBindSQL(dialect, sqlstr) + return nil +} + +// wrapInsertSQL 包装保存Struct语句.返回语句,是否自增,错误信息 +// 数组传递,如果外部方法有调用append的逻辑,append会破坏指针引用,所以传递指针 +// wrapInsertSQL Pack and save 'Struct' statement. Return SQL statement, whether it is incremented, error message +// Array transfer, if the external method has logic to call append, append will destroy the pointer reference, so the pointer is passed +func wrapInsertSQL(ctx context.Context, typeOf *reflect.Type, entity IEntityStruct, columns *[]reflect.StructField, values *[]interface{}) (string, int, string, error) { + sqlstr := "" + inserColumnName, valuesql, autoIncrement, pktype, err := wrapInsertValueSQL(ctx, typeOf, entity, columns, values) + if err != nil { + return sqlstr, autoIncrement, pktype, err + } + + var sqlBuilder strings.Builder + // sqlBuilder.Grow(len(entity.GetTableName()) + len(inserColumnName) + len(entity.GetTableName()) + len(valuesql) + 19) + sqlBuilder.Grow(stringBuilderGrowLen) + // sqlstr := "INSERT INTO " + insersql + " VALUES" + valuesql + sqlBuilder.WriteString("INSERT INTO ") + sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteString(inserColumnName) + sqlBuilder.WriteString(" VALUES") + sqlBuilder.WriteString(valuesql) + sqlstr = sqlBuilder.String() + return sqlstr, autoIncrement, pktype, err +} + +// wrapInsertValueSQL 包装保存Struct语句.返回语句,没有rebuild,返回原始的InsertSQL,ValueSQL,是否自增,主键类型,错误信息 +// 数组传递,如果外部方法有调用append的逻辑,传递指针,因为append会破坏指针引用 +// Pack and save Struct statement. Return SQL statement, no rebuild, return original SQL, whether it is self-increment, error message +// Array transfer, if the external method has logic to call append, append will destroy the pointer reference, so the pointer is passed +func wrapInsertValueSQL(ctx context.Context, typeOf *reflect.Type, entity IEntityStruct, columns *[]reflect.StructField, values *[]interface{}) (string, string, int, string, error) { + var inserColumnName, valuesql string + // 自增类型 0(不自增),1(普通自增),2(序列自增) + // Self-increment type: 0(Not increase),1(Ordinary increment),2(Sequence increment) + autoIncrement := 0 + // 主键类型 + // Primary key type + pktype := "" + // SQL语句的构造器 + // SQL statement constructor + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + // sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteByte('(') + + // SQL语句中,VALUES(?,?,...)语句的构造器 + // In the SQL statement, the constructor of the VALUES(?,?,...) statement + var valueSQLBuilder strings.Builder + valueSQLBuilder.Grow(stringBuilderGrowLen) + valueSQLBuilder.WriteString(" (") + // 主键的名称 + // The name of the primary key. + pkFieldName, e := entityPKFieldName(entity, typeOf) + if e != nil { + return inserColumnName, valuesql, autoIncrement, pktype, e + } + + sequence := entity.GetPkSequence() + if sequence != "" { + // 序列自增 Sequence increment + autoIncrement = 2 + } + + for i := 0; i < len(*columns); i++ { + field := (*columns)[i] + + if field.Name == pkFieldName { // 如果是主键 | If it is the primary key + // 获取主键类型 | Get the primary key type. + pkKind := field.Type.Kind() + switch pkKind { + case reflect.String: + pktype = "string" + case reflect.Int, reflect.Int32, reflect.Int16, reflect.Int8: + pktype = "int" + case reflect.Int64: + pktype = "int64" + default: + return inserColumnName, valuesql, autoIncrement, pktype, errors.New("->wrapInsertValueSQL不支持的主键类型") + } + + // 主键的值 + // The value of the primary key + pkValue := (*values)[i] + valueIsZero := reflect.ValueOf(pkValue).IsZero() + if autoIncrement == 2 { // 如果是序列自增 | If it is a sequence increment + // 去掉这一列,后续不再处理 + // Remove this column and will not process it later. + *columns = append((*columns)[:i], (*columns)[i+1:]...) + *values = append((*values)[:i], (*values)[i+1:]...) + i = i - 1 + if i > 0 { // i+1 0 { // i+1wrapInsertSliceSQL对象数组不能为空") + } + + // 第一个对象,获取第一个Struct对象,用于获取数据库字段,也获取了值 + // The first object, get the first Struct object, used to get the database field, and also get the value + entity := entityStructSlice[0] + + // 先生成一条语句 + // Generate a statement first + inserColumnName, valuesql, autoIncrement, _, firstErr := wrapInsertValueSQL(ctx, typeOf, entity, columns, values) + if firstErr != nil { + return sqlstr, autoIncrement, firstErr + } + var sqlBuilder strings.Builder + // sqlBuilder.Grow(len(entity.GetTableName()) + len(inserColumnName) + len(entity.GetTableName()) + len(valuesql) + 19) + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString("INSERT INTO ") + sqlBuilder.WriteString(entity.GetTableName()) + // sqlstr := "INSERT INTO " + if config.Dialect == "tdengine" && !config.TDengineInsertsColumnName { // 如果是tdengine,拼接类似 INSERT INTO table1 values('2','3') table2 values('4','5'),目前要求字段和类型必须一致,如果不一致,改动略多 + } else { + // sqlstr = sqlstr + insertsql + " VALUES" + valuesql + sqlBuilder.WriteString(inserColumnName) + } + sqlBuilder.WriteString(" VALUES") + sqlBuilder.WriteString(valuesql) + // 如果只有一个Struct对象 + // If there is only one Struct object + if sliceLen == 1 { + return sqlBuilder.String(), autoIncrement, firstErr + } + // 主键的名称 + // The name of the primary key + pkFieldName, e := entityPKFieldName(entity, typeOf) + if e != nil { + return sqlBuilder.String(), autoIncrement, e + } + + for i := 1; i < sliceLen; i++ { + // 拼接字符串 + // Splicing string + if config.Dialect == "tdengine" { // 如果是tdengine,拼接类似 INSERT INTO table1 values('2','3') table2 values('4','5'),目前要求字段和类型必须一致,如果不一致,改动略多 + sqlBuilder.WriteByte(' ') + sqlBuilder.WriteString(entityStructSlice[i].GetTableName()) + if config.TDengineInsertsColumnName { + sqlBuilder.WriteString(inserColumnName) + } + sqlBuilder.WriteString(" VALUES") + sqlBuilder.WriteString(valuesql) + } else { // 标准语法 类似 INSERT INTO table1(id,name) values('2','3'),('4','5') + sqlBuilder.WriteByte(',') + sqlBuilder.WriteString(valuesql) + } + + entityStruct := entityStructSlice[i] + for j := 0; j < len(*columns); j++ { + // 获取实体类的反射,指针下的struct + // Get the reflection of the entity class, the struct under the pointer + valueOf := reflect.ValueOf(entityStruct).Elem() + field := (*columns)[j] + // 字段的值 + // The value of the primary key + fieldValue := valueOf.FieldByName(field.Name) + if field.Name == pkFieldName { // 如果是主键 | If it is the primary key + pkKind := field.Type.Kind() + // pkValue := valueOf.FieldByName(field.Name).Interface() + // 只处理字符串类型的主键,其他类型,columns中并不包含 + // Only handle primary keys of string type, other types, not included in columns + if (pkKind == reflect.String) && fieldValue.IsZero() { + // 主键是字符串类型,并且值为"",赋值'id' + // 生成主键字符串 + // The primary key is a string type, and the value is "", assigned the value'id' + // Generate primary key string + id := FuncGenerateStringID(ctx) + *values = append(*values, id) + // 给对象主键赋值 + // Assign a value to the primary key of the object + fieldValue.Set(reflect.ValueOf(id)) + continue + } + } + + // 给字段赋值 + // Assign a value to the field. + *values = append(*values, fieldValue.Interface()) + + } + } + + sqlstr = sqlBuilder.String() + return sqlstr, autoIncrement, nil +} + +// wrapInsertEntityMapSliceSQL 包装批量保存EntityMapSlice语句.返回语句,值,错误信息 +func wrapInsertEntityMapSliceSQL(ctx context.Context, config *DataSourceConfig, entityMapSlice []IEntityMap) (string, []interface{}, error) { + sliceLen := len(entityMapSlice) + sqlstr := "" + if entityMapSlice == nil || sliceLen < 1 { + return sqlstr, nil, errors.New("->wrapInsertSliceSQL对象数组不能为空") + } + // 第一个对象,获取第一个Struct对象,用于获取数据库字段,也获取了值 + entity := entityMapSlice[0] + // 检查是否是指针对象 + _, err := checkEntityKind(entity) + if err != nil { + return sqlstr, nil, err + } + dbFieldMapKey := entity.GetDBFieldMapKey() + // SQL语句 + inserColumnName, valuesql, values, _, err := wrapInsertValueEntityMapSQL(entity) + if err != nil { + return sqlstr, values, err + } + + var sqlBuilder strings.Builder + // sqlBuilder.Grow(len(entity.GetTableName()) + len(inserColumnName) + len(valuesql) + 19) + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString("INSERT INTO ") + sqlBuilder.WriteString(entity.GetTableName()) + // sqlstr = sqlstr + insertsql + " VALUES" + valuesql + sqlBuilder.WriteString(inserColumnName) + sqlBuilder.WriteString(" VALUES") + sqlBuilder.WriteString(valuesql) + for i := 1; i < sliceLen; i++ { + // 拼接字符串 + // Splicing string + if config.Dialect == "tdengine" { // 如果是tdengine,拼接类似 INSERT INTO table1 values('2','3') table2 values('4','5'),目前要求字段和类型必须一致,如果不一致,改动略多 + sqlBuilder.WriteByte(' ') + sqlBuilder.WriteString(entityMapSlice[i].GetTableName()) + if config.TDengineInsertsColumnName { + sqlBuilder.WriteString(inserColumnName) + } + sqlBuilder.WriteString(" VALUES") + sqlBuilder.WriteString(valuesql) + } else { // 标准语法 类似 INSERT INTO table1(id,name) values('2','3'), values('4','5') + sqlBuilder.WriteByte(',') + sqlBuilder.WriteString(valuesql) + } + + entityMap := entityMapSlice[i] + for j := 0; j < len(dbFieldMapKey); j++ { + key := dbFieldMapKey[j] + value := entityMap.GetDBFieldMap()[key] + values = append(values, value) + } + } + + sqlstr = sqlBuilder.String() + return sqlstr, values, err +} + +// wrapUpdateSQL 包装更新Struct语句 +// 数组传递,如果外部方法有调用append的逻辑,append会破坏指针引用,所以传递指针 +// wrapUpdateSQL Package update Struct statement +// Array transfer, if the external method has logic to call append, append will destroy the pointer reference, so the pointer is passed +func wrapUpdateSQL(typeOf *reflect.Type, entity IEntityStruct, columns *[]reflect.StructField, values *[]interface{}, onlyUpdateNotZero bool) (string, error) { + sqlstr := "" + // SQL语句的构造器 + // SQL statement constructor + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString("UPDATE ") + sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteString(" SET ") + + // 主键的值 + // The value of the primary key + var pkValue interface{} + // 主键的名称 + // The name of the primary key + pkFieldName, e := entityPKFieldName(entity, typeOf) + if e != nil { + return sqlstr, e + } + + for i := 0; i < len(*columns); i++ { + field := (*columns)[i] + if field.Name == pkFieldName { + // 如果是主键 + // If it is the primary key. + pkValue = (*values)[i] + // 去掉这一列,最后处理主键 + // Remove this column, and finally process the primary key + *columns = append((*columns)[:i], (*columns)[i+1:]...) + *values = append((*values)[:i], (*values)[i+1:]...) + i = i - 1 + continue + } + + // 如果是默认值字段,删除掉,不更新 + // If it is the default value field, delete it and do not update + if onlyUpdateNotZero && (reflect.ValueOf((*values)[i]).IsZero()) { + // 去掉这一列,不再处理 + // Remove this column and no longer process + *columns = append((*columns)[:i], (*columns)[i+1:]...) + *values = append((*values)[:i], (*values)[i+1:]...) + i = i - 1 + continue + + } + if i > 0 { + sqlBuilder.WriteByte(',') + } + colName := getFieldTagName(&field) + sqlBuilder.WriteString(colName) + sqlBuilder.WriteString("=?") + + } + // 主键的值是最后一个 + // The value of the primary key is the last + *values = append(*values, pkValue) + + // sqlstr = sqlstr + " WHERE " + entity.GetPKColumnName() + "=?" + sqlBuilder.WriteString(" WHERE ") + sqlBuilder.WriteString(entity.GetPKColumnName()) + sqlBuilder.WriteString("=?") + sqlstr = sqlBuilder.String() + + return sqlstr, nil +} + +// wrapDeleteSQL 包装删除Struct语句 +// wrapDeleteSQL Package delete Struct statement +func wrapDeleteSQL(entity IEntityStruct) (string, error) { + // SQL语句的构造器 + // SQL statement constructor + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString("DELETE FROM ") + sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteString(" WHERE ") + sqlBuilder.WriteString(entity.GetPKColumnName()) + sqlBuilder.WriteString("=?") + sqlstr := sqlBuilder.String() + + return sqlstr, nil +} + +// wrapInsertEntityMapSQL 包装保存Map语句,Map因为没有字段属性,无法完成Id的类型判断和赋值,需要确保Map的值是完整的 +// wrapInsertEntityMapSQL Pack and save the Map statement. Because Map does not have field attributes, +// it cannot complete the type judgment and assignment of Id. It is necessary to ensure that the value of Map is complete +func wrapInsertEntityMapSQL(entity IEntityMap) (string, []interface{}, bool, error) { + sqlstr := "" + inserColumnName, valuesql, values, autoIncrement, err := wrapInsertValueEntityMapSQL(entity) + if err != nil { + return sqlstr, nil, autoIncrement, err + } + // 拼接SQL语句,带上列名,因为Map取值是无序的 + // sqlstr := "INSERT INTO " + insertsql + " VALUES" + valuesql + + var sqlBuilder strings.Builder + // sqlBuilder.Grow(len(inserColumnName) + len(entity.GetTableName()) + len(valuesql) + 19) + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString("INSERT INTO ") + sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteString(inserColumnName) + sqlBuilder.WriteString(" VALUES") + sqlBuilder.WriteString(valuesql) + sqlstr = sqlBuilder.String() + + return sqlstr, values, autoIncrement, nil +} + +// wrapInsertValueEntityMapSQL 包装保存Map语句,Map因为没有字段属性,无法完成Id的类型判断和赋值,需要确保Map的值是完整的 +// wrapInsertValueEntityMapSQL Pack and save the Map statement. Because Map does not have field attributes, +// it cannot complete the type judgment and assignment of Id. It is necessary to ensure that the value of Map is complete +func wrapInsertValueEntityMapSQL(entity IEntityMap) (string, string, []interface{}, bool, error) { + var inserColumnName, valuesql string + // 是否自增,默认false + autoIncrement := false + dbFieldMap := entity.GetDBFieldMap() + if len(dbFieldMap) < 1 { + return inserColumnName, inserColumnName, nil, autoIncrement, errors.New("->wrapInsertEntityMapSQL-->GetDBFieldMap返回值不能为空") + } + // SQL对应的参数 + // SQL corresponding parameters + values := []interface{}{} + + // SQL语句的构造器 + // SQL statement constructor + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + // sqlBuilder.WriteString("INSERT INTO ") + // sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteByte('(') + + // SQL语句中,VALUES(?,?,...)语句的构造器 + // In the SQL statement, the constructor of the VALUES(?,?,...) statement. + var valueSQLBuilder strings.Builder + valueSQLBuilder.Grow(stringBuilderGrowLen) + valueSQLBuilder.WriteString(" (") + // 是否Set了主键 + // Whether the primary key is set. + _, hasPK := dbFieldMap[entity.GetPKColumnName()] + if entity.GetPKColumnName() != "" && !hasPK { // 如果有主键字段,却没值,认为是自增或者序列 | If the primary key is not set, it is considered to be auto-increment or sequence + autoIncrement = true + if entity.GetEntityMapPkSequence() != "" { // 如果是序列 | If it is a sequence. + sqlBuilder.WriteString(entity.GetPKColumnName()) + valueSQLBuilder.WriteString(entity.GetEntityMapPkSequence()) + if len(dbFieldMap) > 1 { // 如果不只有序列 + sqlBuilder.WriteByte(',') + valueSQLBuilder.WriteByte(',') + } + + } + } + + dbFieldMapKey := entity.GetDBFieldMapKey() + for dbFieldMapIndex := 0; dbFieldMapIndex < len(dbFieldMapKey); dbFieldMapIndex++ { + if dbFieldMapIndex > 0 { + sqlBuilder.WriteByte(',') + valueSQLBuilder.WriteByte(',') + } + k := dbFieldMapKey[dbFieldMapIndex] + v := dbFieldMap[k] + // 拼接字符串 + // Concatenated string + sqlBuilder.WriteString(k) + valueSQLBuilder.WriteByte('?') + values = append(values, v) + } + + sqlBuilder.WriteByte(')') + valueSQLBuilder.WriteByte(')') + inserColumnName = sqlBuilder.String() + valuesql = valueSQLBuilder.String() + + return inserColumnName, valuesql, values, autoIncrement, nil +} + +// wrapUpdateEntityMapSQL 包装Map更新语句,Map因为没有字段属性,无法完成Id的类型判断和赋值,需要确保Map的值是完整的 +// wrapUpdateEntityMapSQL Wrap the Map update statement. Because Map does not have field attributes, +// it cannot complete the type judgment and assignment of Id. It is necessary to ensure that the value of Map is complete +func wrapUpdateEntityMapSQL(entity IEntityMap) (string, []interface{}, error) { + dbFieldMap := entity.GetDBFieldMap() + sqlstr := "" + if len(dbFieldMap) < 1 { + return sqlstr, nil, errors.New("->wrapUpdateEntityMapSQL-->GetDBFieldMap返回值不能为空") + } + // SQL语句的构造器 + // SQL statement constructor + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString("UPDATE ") + sqlBuilder.WriteString(entity.GetTableName()) + sqlBuilder.WriteString(" SET ") + + // SQL对应的参数 + // SQL corresponding parameters + values := []interface{}{} + // 主键名称 + // Primary key name + var pkValue interface{} + dbFieldMapIndex := 0 + for k, v := range dbFieldMap { + + if k == entity.GetPKColumnName() { // 如果是主键 | If it is the primary key + pkValue = v + continue + } + if dbFieldMapIndex > 0 { + sqlBuilder.WriteByte(',') + } + + // 拼接字符串 | Splicing string. + sqlBuilder.WriteString(k) + sqlBuilder.WriteString("=?") + values = append(values, v) + dbFieldMapIndex++ + } + // 主键的值是最后一个 + // The value of the primary key is the last + values = append(values, pkValue) + + sqlBuilder.WriteString(" WHERE ") + sqlBuilder.WriteString(entity.GetPKColumnName()) + sqlBuilder.WriteString("=?") + sqlstr = sqlBuilder.String() + + return sqlstr, values, nil +} + +// wrapQuerySQL 封装查询语句 +// wrapQuerySQL Encapsulated query statement +func wrapQuerySQL(dialect string, finder *Finder, page *Page) (string, error) { + // 获取到没有page的sql的语句 + // Get the SQL statement without page. + sqlstr, err := finder.GetSQL() + if err != nil { + return "", err + } + if page != nil { + err = wrapPageSQL(dialect, &sqlstr, page) + } + if err != nil { + return "", err + } + return sqlstr, err +} + +// 查询'order by'在sql中出现的开始位置和结束位置 +// Query the start position and end position of'order by' in SQL +var ( + orderByExpr = "(?i)\\s(order)\\s+by\\s" + orderByRegexp, _ = regexp.Compile(orderByExpr) +) + +// findOrderByIndex 查询order by在sql中出现的开始位置和结束位置 +// findOrderByIndex Query the start position and end position of'order by' in SQL +func findOrderByIndex(strsql *string) []int { + loc := orderByRegexp.FindStringIndex(*strsql) + return loc +} + +// 查询'group by'在sql中出现的开始位置和结束位置 +// Query the start position and end position of'group by' in sql。 +var ( + groupByExpr = "(?i)\\s(group)\\s+by\\s" + groupByRegexp, _ = regexp.Compile(groupByExpr) +) + +// findGroupByIndex 查询group by在sql中出现的开始位置和结束位置 +// findGroupByIndex Query the start position and end position of'group by' in sql +func findGroupByIndex(strsql *string) []int { + loc := groupByRegexp.FindStringIndex(*strsql) + return loc +} + +// 查询 from 在sql中出现的开始位置和结束位置 +// Query the start position and end position of 'from' in sql +// var fromExpr = "(?i)(^\\s*select)(.+?\\(.+?\\))*.*?(from)" +// 感谢奔跑(@zeqjone)提供的正则,排除不在括号内的from,已经满足绝大部分场景, +// select id1,(select (id2) from t1 where id=2) _s FROM table select的子查询 _s中的 id2还有括号,才会出现问题,建议使用CountFinder处理分页语句 +// countFinder := zorm.NewFinder().Append("select count(*) from (") +// countFinder.AppendFinder(finder) +// countFinder.Append(") tempcountfinder") +// finder.CountFinder = countFinder +var ( + fromExpr = "(?i)(^\\s*select)(\\(.*?\\)|[^()]+)*?(from)" + fromRegexp, _ = regexp.Compile(fromExpr) +) + +// findFromIndexa 查询from在sql中出现的开始位置和结束位置 +// findSelectFromIndex Query the start position and end position of 'from' in sql +func findSelectFromIndex(strsql *string) []int { + // 匹配出来的是完整的字符串,用最后的FROM即可 + loc := fromRegexp.FindStringIndex(*strsql) + if len(loc) < 2 { + return loc + } + // 最后的FROM前推4位字符串 + loc[0] = loc[1] - 4 + return loc +} + +/* +var fromExpr = `\(([\s\S]+?)\)` +var fromRegexp, _ = regexp.Compile(fromExpr) + +//查询 from 在sql中出现的开始位置 +//Query the start position of 'from' in sql +func findSelectFromIndex(strsql string) int { + sql := strings.ToLower(strsql) + m := fromRegexp.FindAllString(sql, -1) + for i := 0; i < len(m); i++ { + str := m[i] + strnofrom := strings.ReplaceAll(str, " from ", " zorm ") + sql = strings.ReplaceAll(sql, str, strnofrom) + } + fromIndex := strings.LastIndex(sql, " from ") + if fromIndex < 0 { + return fromIndex + } + //补上一个空格 + fromIndex = fromIndex + 1 + return fromIndex +} +*/ +/* +// 从更新语句中获取表名 +//update\\s(.+)set\\s.* +var ( + updateExper = "(?i)^\\s*update\\s+(\\w+)\\s+set\\s" + updateRegexp, _ = regexp.Compile(updateExper) +) + +// findUpdateTableName 获取语句中表名 +// 第一个是符合的整体数据,第二个是表名 +func findUpdateTableName(strsql *string) []string { + matchs := updateRegexp.FindStringSubmatch(*strsql) + return matchs +} + +// 从删除语句中获取表名 +// delete\\sfrom\\s(.+)where\\s(.*) +var ( + deleteExper = "(?i)^\\s*delete\\s+from\\s+(\\w+)\\s+where\\s" + deleteRegexp, _ = regexp.Compile(deleteExper) +) + +// findDeleteTableName 获取语句中表名 +// 第一个是符合的整体数据,第二个是表名 +func findDeleteTableName(strsql *string) []string { + matchs := deleteRegexp.FindStringSubmatch(*strsql) + return matchs +} +*/ + +// FuncGenerateStringID 默认生成字符串ID的函数.方便自定义扩展 +// FuncGenerateStringID Function to generate string ID by default. Convenient for custom extension +var FuncGenerateStringID = func(ctx context.Context) string { + // 使用 crypto/rand 真随机9位数 + randNum, randErr := rand.Int(rand.Reader, big.NewInt(1000000000)) + if randErr != nil { + return "" + } + // 获取9位数,前置补0,确保9位数 + rand9 := fmt.Sprintf("%09d", randNum) + + // 获取纳秒 按照 年月日时分秒毫秒微秒纳秒 拼接为长度23位的字符串 + pk := time.Now().Format("2006.01.02.15.04.05.000000000") + pk = strings.ReplaceAll(pk, ".", "") + + // 23位字符串+9位随机数=32位字符串,这样的好处就是可以使用ID进行排序 + pk = pk + rand9 + return pk +} + +// FuncWrapFieldTagName 用于包裹字段名, eg. `describe` +var FuncWrapFieldTagName = func(colName string) string { + // custom: return fmt.Sprintf("`%s`", colName) + return colName +} + +// getFieldTagName 获取模型中定义的数据库的 column tag +func getFieldTagName(field *reflect.StructField) string { + colName := field.Tag.Get(tagColumnName) + colName = FuncWrapFieldTagName(colName) + /* + if dialect == "kingbase" { + // kingbase R3 驱动大小写敏感,通常是大写。数据库全的列名部换成双引号括住的大写字符,避免与数据库内置关键词冲突时报错 + colName = strings.ReplaceAll(colName, "\"", "") + colName = fmt.Sprintf(`"%s"`, strings.ToUpper(colName)) + } + */ + return colName +} + +// wrapSQLHint 在sql语句中增加hint +func wrapSQLHint(ctx context.Context, sqlstr *string) error { + // 获取hint + contextValue := ctx.Value(contextSQLHintValueKey) + if contextValue == nil { // 如果没有设置hint + return nil + } + hint, ok := contextValue.(string) + if !ok { + return errors.New("->wrapSQLHint-->contextValue转换string失败") + } + if hint == "" { + return nil + } + sqlByte := []byte(*sqlstr) + // 获取第一个单词 + _, start, end, err := firstOneWord(0, &sqlByte) + if err != nil { + return err + } + if start == -1 || end == -1 { // 未取到字符串 + return nil + } + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString((*sqlstr)[:end]) + sqlBuilder.WriteByte(' ') + sqlBuilder.WriteString(hint) + sqlBuilder.WriteString((*sqlstr)[end:]) + *sqlstr = sqlBuilder.String() + return nil +} + +// reBindSQL 包装基础的SQL语句,根据数据库类型,调整SQL变量符号,例如?,? $1,$2这样的 +// reBindSQL Pack basic SQL statements, adjust the SQL variable symbols according to the database type, such as?,? $1,$2 +func reBindSQL(dialect string, sqlstr *string, args *[]interface{}) (*string, *[]interface{}, error) { + argsNum := len(*args) + if argsNum < 1 { // 没有参数,不需要处理,也不判断参数数量了,数据库会报错提示 + return sqlstr, args, nil + } + // 重新记录参数值 + // Re-record the parameter value + newValues := make([]interface{}, 0) + // 记录sql参数值的下标,例如 $1 @p1 ,从1开始 + sqlParamIndex := 1 + + // 新的sql + // new sql + var newSQLStr strings.Builder + // newSQLStr.Grow(len(*sqlstr)) + newSQLStr.Grow(stringBuilderGrowLen) + i := -1 + for _, v := range []byte(*sqlstr) { + if v != '?' { // 如果不是?问号 + newSQLStr.WriteByte(v) + continue + } + i = i + 1 + if i >= argsNum { // 占位符数量比参数值多,不使用 strings.Count函数,避免多次操作字符串 + return nil, nil, fmt.Errorf("sql语句中参数和值数量不一致,-->zormErrorExecSQL:%s,-->zormErrorSQLValues:%v", *sqlstr, *args) + } + v := (*args)[i] + // 反射获取参数的值 + valueOf := reflect.ValueOf(v) + // 获取类型 + kind := valueOf.Kind() + // 如果参数是个指针类型 + // If the parameter is a pointer type + if kind == reflect.Ptr { // 如果是指针 | If it is a pointer + valueOf = valueOf.Elem() + kind = valueOf.Kind() + } + typeOf := valueOf.Type() + // 参数值长度,默认是1,其他取值数组长度 + valueLen := 1 + + // 如果不是数组或者slice + // If it is not an array or slice + if !(kind == reflect.Array || kind == reflect.Slice) { + // 记录新值 + // Record new value. + newValues = append(newValues, v) + } else if typeOf == reflect.TypeOf([]byte{}) { + // 记录新值 + // Record new value + newValues = append(newValues, v) + } else { + // 如果不是字符串类型的值,无法取长度,这个是个bug,先注释了 + // 获取数组类型参数值的长度 + // If it is not a string type value, the length cannot be taken, this is a bug, first comment + // Get the length of the array type parameter value + valueLen = valueOf.Len() + // 数组类型的参数长度小于1,认为是有异常的参数 + // The parameter length of the array type is less than 1, which is considered to be an abnormal parameter + if valueLen < 1 { + return nil, nil, errors.New("->reBindSQL()语句:" + *sqlstr + ",第" + strconv.Itoa(i+1) + "个参数,类型是Array或者Slice,值的长度为0,请检查sql参数有效性") + } else if valueLen == 1 { // 如果数组里只有一个参数,认为是单个参数 + v = valueOf.Index(0).Interface() + newValues = append(newValues, v) + } + + } + + switch dialect { + case "mysql", "sqlite", "dm", "gbase", "clickhouse", "db2": + wrapParamSQL("?", valueLen, &sqlParamIndex, &newSQLStr, &valueOf, &newValues, false, false) + case "postgresql", "kingbase": // postgresql,kingbase + wrapParamSQL("$", valueLen, &sqlParamIndex, &newSQLStr, &valueOf, &newValues, true, false) + case "mssql": // mssql + wrapParamSQL("@p", valueLen, &sqlParamIndex, &newSQLStr, &valueOf, &newValues, true, false) + case "oracle", "shentong": // oracle,神通 + wrapParamSQL(":", valueLen, &sqlParamIndex, &newSQLStr, &valueOf, &newValues, true, false) + case "tdengine": // tdengine,重新处理 字符类型的参数 '?' + wrapParamSQL("?", valueLen, &sqlParamIndex, &newSQLStr, &valueOf, &newValues, false, true) + default: // 其他情况,还是使用 ? | In other cases, or use ? + newSQLStr.WriteByte('?') + } + + } + + //?号占位符的数量和参数不一致,不使用 strings.Count函数,避免多次操作字符串 + if (i + 1) != argsNum { + return nil, nil, fmt.Errorf("sql语句中参数和值数量不一致,-->zormErrorExecSQL:%s,-->zormErrorSQLValues:%v", *sqlstr, *args) + } + sqlstring := newSQLStr.String() + return &sqlstring, &newValues, nil +} + +// reUpdateFinderSQL 根据数据类型更新 手动编写的 UpdateFinder的语句,用于处理数据库兼容,例如 clickhouse的 UPDATE 和 DELETE +func reUpdateSQL(dialect string, sqlstr *string) error { + if dialect != "clickhouse" { // 目前只处理clickhouse + return nil + } + // 处理clickhouse的特殊更新语法 + sqlByte := []byte(*sqlstr) + // 获取第一个单词 + firstWord, start, end, err := firstOneWord(0, &sqlByte) + if err != nil { + return err + } + if start == -1 || end == -1 { // 未取到字符串 + return nil + } + // SQL语句的构造器 + // SQL statement constructor + var sqlBuilder strings.Builder + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString((*sqlstr)[:start]) + sqlBuilder.WriteString("ALTER TABLE ") + firstWord = strings.ToUpper(firstWord) + tableName := "" + if firstWord == "UPDATE" { // 更新 update tableName set + tableName, _, end, err = firstOneWord(end, &sqlByte) + if err != nil { + return err + } + // 拿到 set + _, start, end, err = firstOneWord(end, &sqlByte) + + } else if firstWord == "DELETE" { // 删除 delete from tableName + // 拿到from + _, _, end, err = firstOneWord(end, &sqlByte) + if err != nil { + return err + } + // 拿到 tableName + tableName, start, end, err = firstOneWord(end, &sqlByte) + } else { // 只处理UPDATE 和 DELETE 语法 + return nil + } + if err != nil { + return err + } + if start == -1 || end == -1 { // 获取的位置异常 + return errors.New("->reUpdateSQL中clickhouse语法异常,请检查sql语句是否标准,-->zormErrorExecSQL:" + *sqlstr) + } + sqlBuilder.WriteString(tableName) + sqlBuilder.WriteByte(' ') + sqlBuilder.WriteString(firstWord) + // sqlBuilder.WriteByte(' ') + sqlBuilder.WriteString((*sqlstr)[end:]) + *sqlstr = sqlBuilder.String() + return nil +} + +// wrapAutoIncrementInsertSQL 包装自增的自增主键的插入sql +func wrapAutoIncrementInsertSQL(pkColumnName string, sqlstr *string, dialect string, values *[]interface{}) (*int64, *int64) { + // oracle 12c+ 支持IDENTITY属性的自增列,因为分页也要求12c+的语法,所以数据库就IDENTITY创建自增吧 + // 处理序列产生的自增主键,例如oracle,postgresql等 + var lastInsertID, zormSQLOutReturningID *int64 + var sqlBuilder strings.Builder + // sqlBuilder.Grow(len(*sqlstr) + len(pkColumnName) + 40) + sqlBuilder.Grow(stringBuilderGrowLen) + sqlBuilder.WriteString(*sqlstr) + switch dialect { + case "postgresql", "kingbase": + var p int64 = 0 + lastInsertID = &p + // sqlstr = sqlstr + " RETURNING " + pkColumnName + sqlBuilder.WriteString(" RETURNING ") + sqlBuilder.WriteString(pkColumnName) + case "oracle", "shentong": + var p int64 = 0 + zormSQLOutReturningID = &p + // sqlstr = sqlstr + " RETURNING " + pkColumnName + " INTO :zormSQLOutReturningID " + sqlBuilder.WriteString(" RETURNING ") + sqlBuilder.WriteString(pkColumnName) + sqlBuilder.WriteString(" INTO :zormSQLOutReturningID ") + v := sql.Named("zormSQLOutReturningID", sql.Out{Dest: zormSQLOutReturningID}) + *values = append(*values, v) + } + + *sqlstr = sqlBuilder.String() + return lastInsertID, zormSQLOutReturningID +} + +// getConfigFromConnection 从dbConnection中获取数据库方言,如果没有,从FuncReadWriteStrategy获取dbDao,获取dbdao.config.Dialect +func getConfigFromConnection(ctx context.Context, dbConnection *dataBaseConnection, rwType int) (*DataSourceConfig, error) { + var config *DataSourceConfig + // dbConnection为nil,使用defaultDao + // dbConnection is nil, use default Dao + if dbConnection == nil { + dbdao, err := FuncReadWriteStrategy(ctx, rwType) + if err != nil { + return nil, err + } + config = dbdao.config + } else { + config = dbConnection.config + } + return config, nil +} + +// wrapParamSQL 包装SQL语句 +// symbols(占位符) valueLen(参数长度) sqlParamIndexPtr(参数的下标指针,数组会改变值) newSQLStr(SQL字符串Builder) valueOf(参数值的反射对象) hasParamIndex(是否拼接参数下标 $1 $2) isTDengine(TDengine数据库需要单独处理字符串类型) +func wrapParamSQL(symbols string, valueLen int, sqlParamIndexPtr *int, newSQLStr *strings.Builder, valueOf *reflect.Value, newValues *[]interface{}, hasParamIndex bool, isTDengine bool) { + sqlParamIndex := *sqlParamIndexPtr + if valueLen == 1 { + if isTDengine && valueOf.Kind() == reflect.String { // 处理tdengine的字符串类型 + symbols = "'?'" + } + newSQLStr.WriteString(symbols) + + if hasParamIndex { + newSQLStr.WriteString(strconv.Itoa(sqlParamIndex)) + } + + } else if valueLen > 1 { // 如果值是数组 + for j := 0; j < valueLen; j++ { + valuej := (*valueOf).Index(j) + if isTDengine && valuej.Kind() == reflect.String { // 处理tdengine的字符串类型 + symbols = "'?'" + } + if j == 0 { // 第一个 + newSQLStr.WriteString(symbols) + } else { + newSQLStr.WriteByte(',') + newSQLStr.WriteString(symbols) + } + if hasParamIndex { + newSQLStr.WriteString(strconv.Itoa(sqlParamIndex + j)) + } + sliceValue := valuej.Interface() + *newValues = append(*newValues, sliceValue) + } + } + *sqlParamIndexPtr = *sqlParamIndexPtr + valueLen +} + +// firstOneWord 从指定下标,获取第一个单词,不包含前后空格,并返回开始下标和结束下标,如果找不到合法的字符串,返回-1 +func firstOneWord(index int, strByte *[]byte) (string, int, int, error) { + start := -1 + end := -1 + byteLen := len(*strByte) + if index < 0 { + return "", start, end, errors.New("->firstOneWord索引小于0") + } + if index > byteLen { // 如果索引大于长度 + return "", start, end, errors.New("->firstOneWord索引大于字符串长度") + } + var newStr strings.Builder + newStr.Grow(10) + for ; index < byteLen; index++ { + v := (*strByte)[index] + if v == '(' || v == ')' { // 不处理括号 + continue + } + if start == -1 && v != ' ' { // 不是空格 + start = index + } + if start == -1 && v == ' ' { // 空格 + continue + } + if start >= 0 && v != ' ' { // 需要的字符 + newStr.WriteByte(v) + } else { // 遇到空格结束记录 + end = index + break + } + } + if start >= 0 && end == -1 { // 记录到结尾,不是空格结束 + end = byteLen + } + + return newStr.String(), start, end, nil +} diff --git a/vendor/gitee.com/chunanyong/zorm/structFieldInfo.go b/vendor/gitee.com/chunanyong/zorm/structFieldInfo.go new file mode 100644 index 00000000..9ef3f1d7 --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/structFieldInfo.go @@ -0,0 +1,564 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "context" + "database/sql" + "errors" + "fmt" + "go/ast" + "reflect" + "strings" + "sync" +) + +const ( + // tag标签的名称 + tagColumnName = "column" + + // 输出字段 缓存的前缀 + exportPrefix = "_exportStructFields_" + // 私有字段 缓存的前缀 + privatePrefix = "_privateStructFields_" + // 数据库列名 缓存的前缀 + dbColumnNamePrefix = "_dbColumnName_" + + // 数据库所有列名,经过排序 缓存的前缀 + dbColumnNameSlicePrefix = "_dbColumnNameSlice_" + + // field对应的column的tag值 缓存的前缀 + // structFieldTagPrefix = "_structFieldTag_" + // 数据库主键 缓存的前缀 + // dbPKNamePrefix = "_dbPKName_" +) + +// cacheStructFieldInfoMap 用于缓存反射的信息,sync.Map内部处理了并发锁 +var cacheStructFieldInfoMap *sync.Map = &sync.Map{} + +// var cacheStructFieldInfoMap = make(map[string]map[string]reflect.StructField) + +// 用于缓存field对应的column的tag值 +// var cacheStructFieldTagInfoMap = make(map[string]map[string]string) + +// structFieldInfo 获取StructField的信息.只对struct或者*struct判断,如果是指针,返回指针下实际的struct类型 +// 第一个返回值是可以输出的字段(首字母大写),第二个是不能输出的字段(首字母小写) +func structFieldInfo(typeOf *reflect.Type) error { + if typeOf == nil { + return errors.New("->structFieldInfo数据为空") + } + + entityName := (*typeOf).String() + + // 缓存的key + // 所有输出的属性,包含数据库字段,key是struct属性的名称,不区分大小写 + exportCacheKey := exportPrefix + entityName + // 所有私有变量的属性,key是struct属性的名称,不区分大小写 + privateCacheKey := privatePrefix + entityName + // 所有数据库的属性,key是数据库的字段名称,不区分大小写 + dbColumnCacheKey := dbColumnNamePrefix + entityName + // 所有数据库字段名称的slice,经过排序,不区分大小写 + dbColumnNameSliceCacheKey := dbColumnNameSlicePrefix + entityName + + // structFieldTagCacheKey := structFieldTagPrefix + entityName + // dbPKNameCacheKey := dbPKNamePrefix + entityName + // 缓存的数据库主键值 + _, exportOk := cacheStructFieldInfoMap.Load(exportCacheKey) + //_, exportOk := cacheStructFieldInfoMap[exportCacheKey] + //如果存在值,认为缓存中有所有的信息,不再处理 + if exportOk { + return nil + } + // 获取字段长度 + fieldNum := (*typeOf).NumField() + // 如果没有字段 + if fieldNum < 1 { + return errors.New("->structFieldInfo-->NumField entity没有属性") + } + + // 声明所有字段的载体 + var allFieldMap *sync.Map = &sync.Map{} + // anonymous := make([]reflect.StructField, 0) + + // 缓存的数据 + exportStructFieldMap := make(map[string]reflect.StructField) + privateStructFieldMap := make(map[string]reflect.StructField) + dbColumnFieldMap := make(map[string]reflect.StructField) + + // structFieldTagMap := make(map[string]string) + dbColumnFieldNameSlice := make([]string, 0) + + // 遍历sync.Map,要求输入一个func作为参数 + // 这个函数的入参、出参的类型都已经固定,不能修改 + // 可以在函数体内编写自己的代码,调用map中的k,v + // var funcMapKV func(k, v interface{}) bool + funcMapKV := func(k, v interface{}) bool { + field := v.(reflect.StructField) + fieldName := field.Name + if ast.IsExported(fieldName) { // 如果是可以输出的,不区分大小写 + exportStructFieldMap[strings.ToLower(fieldName)] = field + // 如果是数据库字段 + tagColumnValue := field.Tag.Get(tagColumnName) + if len(tagColumnValue) > 0 { + // dbColumnFieldMap[tagColumnValue] = field + // 使用数据库字段的小写,处理oracle和达梦数据库的sql返回值大写 + tagColumnValueLower := strings.ToLower(tagColumnValue) + dbColumnFieldMap[tagColumnValueLower] = field + dbColumnFieldNameSlice = append(dbColumnFieldNameSlice, tagColumnValueLower) + // structFieldTagMap[fieldName] = tagColumnValue + } + + } else { // 私有属性 + privateStructFieldMap[strings.ToLower(fieldName)] = field + } + + return true + } + // 并发锁,用于处理slice并发append + var lock sync.Mutex + // funcRecursiveAnonymous 递归调用struct的匿名属性,就近覆盖属性 + var funcRecursiveAnonymous func(allFieldMap *sync.Map, anonymous *reflect.StructField) + funcRecursiveAnonymous = func(allFieldMap *sync.Map, anonymous *reflect.StructField) { + // 字段类型 + anonymousTypeOf := anonymous.Type + if anonymousTypeOf.Kind() == reflect.Ptr { + // 获取指针下的Struct类型 + anonymousTypeOf = anonymousTypeOf.Elem() + } + + // 只处理Struct类型 + if anonymousTypeOf.Kind() != reflect.Struct { + return + } + + // 获取字段长度 + fieldNum := anonymousTypeOf.NumField() + // 如果没有字段 + if fieldNum < 1 { + return + } + // 遍历所有字段 + for i := 0; i < fieldNum; i++ { + anonymousField := anonymousTypeOf.Field(i) + if anonymousField.Anonymous { // 匿名struct里自身又有匿名struct + funcRecursiveAnonymous(allFieldMap, &anonymousField) + } else if _, ok := allFieldMap.Load(anonymousField.Name); !ok { // 普通命名字段,而且没有记录过 + allFieldMap.Store(anonymousField.Name, anonymousField) + lock.Lock() + funcMapKV(anonymousField.Name, anonymousField) + lock.Unlock() + } + } + } + + // 遍历所有字段,记录匿名属性 + for i := 0; i < fieldNum; i++ { + field := (*typeOf).Field(i) + if field.Anonymous { // 如果是匿名的 + funcRecursiveAnonymous(allFieldMap, &field) + } else if _, ok := allFieldMap.Load(field.Name); !ok { // 普通命名字段,而且没有记录过 + allFieldMap.Store(field.Name, field) + lock.Lock() + funcMapKV(field.Name, field) + lock.Unlock() + } + } + + // allFieldMap.Range(f) + + // 加入缓存 + cacheStructFieldInfoMap.Store(exportCacheKey, exportStructFieldMap) + cacheStructFieldInfoMap.Store(privateCacheKey, privateStructFieldMap) + cacheStructFieldInfoMap.Store(dbColumnCacheKey, dbColumnFieldMap) + // cacheStructFieldInfoMap[exportCacheKey] = exportStructFieldMap + // cacheStructFieldInfoMap[privateCacheKey] = privateStructFieldMap + // cacheStructFieldInfoMap[dbColumnCacheKey] = dbColumnFieldMap + + // cacheStructFieldTagInfoMap[structFieldTagCacheKey] = structFieldTagMap + + // 不按照字母顺序,按照反射获取的Struct属性顺序,生成insert语句和update语句 + // sort.Strings(dbColumnFieldNameSlice) + cacheStructFieldInfoMap.Store(dbColumnNameSliceCacheKey, dbColumnFieldNameSlice) + + return nil +} + +// setFieldValueByColumnName 根据数据库的字段名,找到struct映射的字段,并赋值 +func setFieldValueByColumnName(entity interface{}, columnName string, value interface{}) error { + // 先从本地缓存中查找 + typeOf := reflect.TypeOf(entity) + valueOf := reflect.ValueOf(entity) + if typeOf.Kind() == reflect.Ptr { // 如果是指针 + typeOf = typeOf.Elem() + valueOf = valueOf.Elem() + } + + dbMap, err := getDBColumnFieldMap(&typeOf) + if err != nil { + return err + } + f, ok := dbMap[strings.ToLower(columnName)] + if ok { // 给主键赋值 + valueOf.FieldByName(f.Name).Set(reflect.ValueOf(value)) + } + return nil +} + +// structFieldValue 获取指定字段的值 +func structFieldValue(s interface{}, fieldName string) (interface{}, error) { + if s == nil || len(fieldName) < 1 { + return nil, errors.New("->structFieldValue数据为空") + } + // entity的s类型 + valueOf := reflect.ValueOf(s) + + kind := valueOf.Kind() + if !(kind == reflect.Ptr || kind == reflect.Struct) { + return nil, errors.New("->structFieldValue必须是Struct或者*Struct类型") + } + + if kind == reflect.Ptr { + // 获取指针下的Struct类型 + valueOf = valueOf.Elem() + if valueOf.Kind() != reflect.Struct { + return nil, errors.New("->structFieldValue必须是Struct或者*Struct类型") + } + } + + // FieldByName方法返回的是reflect.Value类型,调用Interface()方法,返回原始类型的数据值 + value := valueOf.FieldByName(fieldName).Interface() + + return value, nil +} + +// getDBColumnExportFieldMap 获取实体类的数据库字段,key是数据库的字段名称.同时返回所有的字段属性的map,key是实体类的属性.不区分大小写 +func getDBColumnExportFieldMap(typeOf *reflect.Type) (map[string]reflect.StructField, map[string]reflect.StructField, error) { + dbColumnFieldMap, err := getCacheStructFieldInfoMap(typeOf, dbColumnNamePrefix) + if err != nil { + return nil, nil, err + } + exportFieldMap, err := getCacheStructFieldInfoMap(typeOf, exportPrefix) + return dbColumnFieldMap, exportFieldMap, err +} + +// getDBColumnFieldMap 获取实体类的数据库字段,key是数据库的字段名称.不区分大小写 +func getDBColumnFieldMap(typeOf *reflect.Type) (map[string]reflect.StructField, error) { + return getCacheStructFieldInfoMap(typeOf, dbColumnNamePrefix) +} + +// getDBColumnFieldNameSlice 获取实体类的数据库字段,经过排序,key是数据库的字段名称.不区分大小写, +func getDBColumnFieldNameSlice(typeOf *reflect.Type) ([]string, error) { + dbColumnFieldSlice, dbmapErr := getCacheStructFieldInfo(typeOf, dbColumnNameSlicePrefix) + if dbmapErr != nil { + return nil, fmt.Errorf("->getDBColumnFieldNameSlice-->getCacheStructFieldInfo()取值错误:%w", dbmapErr) + } + dbcfSlice, efOK := dbColumnFieldSlice.([]string) + if !efOK { + return dbcfSlice, errors.New("->getDBColumnFieldNameSlice-->dbColumnFieldSlice取值转[]string类型异常") + } + return dbcfSlice, nil +} + +// getCacheStructFieldInfo 根据类型和key,获取缓存的数据字段信息slice,已经排序 +func getCacheStructFieldInfo(typeOf *reflect.Type, keyPrefix string) (interface{}, error) { + if typeOf == nil { + return nil, errors.New("->getCacheStructFieldInfo-->typeOf不能为空") + } + key := keyPrefix + (*typeOf).String() + dbColumnFieldMap, dbOk := cacheStructFieldInfoMap.Load(key) + // dbColumnFieldMap, dbOk := cacheStructFieldInfoMap[key] + if !dbOk { // 缓存不存在 + // 获取实体类的输出字段和私有 字段 + err := structFieldInfo(typeOf) + if err != nil { + return nil, err + } + dbColumnFieldMap, dbOk = cacheStructFieldInfoMap.Load(key) + // dbColumnFieldMap, dbOk = cacheStructFieldInfoMap[key] + if !dbOk { + return nil, errors.New("->getCacheStructFieldInfo-->cacheStructFieldInfoMap.Load()获取数据库字段dbColumnFieldMap异常") + } + } + + return dbColumnFieldMap, nil + + // return dbColumnFieldMap, nil +} + +// getCacheStructFieldInfoMap 根据类型和key,获取缓存的字段信息 +func getCacheStructFieldInfoMap(typeOf *reflect.Type, keyPrefix string) (map[string]reflect.StructField, error) { + dbColumnFieldMap, dbmapErr := getCacheStructFieldInfo(typeOf, keyPrefix) + if dbmapErr != nil { + return nil, fmt.Errorf("->getCacheStructFieldInfoMap-->getCacheStructFieldInfo()取值错误:%w", dbmapErr) + } + dbcfMap, efOK := dbColumnFieldMap.(map[string]reflect.StructField) + if !efOK { + return dbcfMap, errors.New("->getCacheStructFieldInfoMap-->dbColumnFieldMap取值转map[string]reflect.StructField类型异常") + } + return dbcfMap, nil + + // return dbColumnFieldMap, nil +} + +// columnAndValue 根据保存的对象,返回插入的语句,需要插入的字段,字段的值 +func columnAndValue(entity interface{}) (reflect.Type, []reflect.StructField, []interface{}, error) { + typeOf, checkerr := checkEntityKind(entity) + if checkerr != nil { + return typeOf, nil, nil, checkerr + } + // 获取实体类的反射,指针下的struct + valueOf := reflect.ValueOf(entity).Elem() + // reflect.Indirect + + // 先从本地缓存中查找 + // typeOf := reflect.TypeOf(entity).Elem() + + dbMap, err := getDBColumnFieldMap(&typeOf) + if err != nil { + return typeOf, nil, nil, err + } + dbSlice, err := getDBColumnFieldNameSlice(&typeOf) + if err != nil { + return typeOf, nil, nil, err + } + // 实体类公开字段的长度 + fLen := len(dbMap) + // 长度不一致 + if fLen-len(dbSlice) != 0 { + return typeOf, nil, nil, errors.New("->columnAndValue-->缓存的数据库字段和实体类字段不对应") + } + // 接收列的数组,这里是做一个副本,避免外部更改掉原始的列信息 + columns := make([]reflect.StructField, 0, fLen) + // 接收值的数组 + values := make([]interface{}, 0, fLen) + + // 遍历所有数据库属性 + for _, fieldName := range dbSlice { + //获取字段类型的Kind + // fieldKind := field.Type.Kind() + //if !allowTypeMap[fieldKind] { //不允许的类型 + // continue + //} + field := dbMap[fieldName] + columns = append(columns, field) + // FieldByName方法返回的是reflect.Value类型,调用Interface()方法,返回原始类型的数据值.字段不会重名,不使用FieldByIndex()函数 + value := valueOf.FieldByName(field.Name).Interface() + // 添加到记录值的数组 + values = append(values, value) + + } + + // 缓存数据库的列 + return typeOf, columns, values, nil +} + +// entityPKFieldName 获取实体类主键属性名称 +func entityPKFieldName(entity IEntityStruct, typeOf *reflect.Type) (string, error) { + //检查是否是指针对象 + //typeOf, checkerr := checkEntityKind(entity) + //if checkerr != nil { + // return "", checkerr + //} + + // 缓存的key,TypeOf和ValueOf的String()方法,返回值不一样 + // typeOf := reflect.TypeOf(entity).Elem() + + dbMap, err := getDBColumnFieldMap(typeOf) + if err != nil { + return "", err + } + field := dbMap[strings.ToLower(entity.GetPKColumnName())] + return field.Name, nil +} + +// checkEntityKind 检查entity类型必须是*struct类型或者基础类型的指针 +func checkEntityKind(entity interface{}) (reflect.Type, error) { + if entity == nil { + return nil, errors.New("->checkEntityKind参数不能为空,必须是*struct类型或者基础类型的指针") + } + typeOf := reflect.TypeOf(entity) + if typeOf.Kind() != reflect.Ptr { // 如果不是指针 + return nil, errors.New("->checkEntityKind必须是*struct类型或者基础类型的指针") + } + typeOf = typeOf.Elem() + //if !(typeOf.Kind() == reflect.Struct || allowBaseTypeMap[typeOf.Kind()]) { //如果不是指针 + // return nil, errors.New("checkEntityKind必须是*struct类型或者基础类型的指针") + //} + return typeOf, nil +} + +// sqlRowsValues 包装接收sqlRows的Values数组,反射rows屏蔽数据库null值,兼容单个字段查询和Struct映射 +// fix:converting NULL to int is unsupported +// 当读取数据库的值为NULL时,由于基本类型不支持为NULL,通过反射将未知driver.Value改为interface{},不再映射到struct实体类 +// 感谢@fastabler提交的pr +// oneColumnScanner 只有一个字段,而且可以直接Scan,例如string或者[]string,不需要反射StructType进行处理 +func sqlRowsValues(ctx context.Context, dialect string, valueOf *reflect.Value, typeOf *reflect.Type, rows *sql.Rows, driverValue *reflect.Value, columnTypes []*sql.ColumnType, entity interface{}, dbColumnFieldMap, exportFieldMap *map[string]reflect.StructField) error { + if entity == nil && valueOf == nil { + return errors.New("->sqlRowsValues-->valueOfElem为nil") + } + + var valueOfElem reflect.Value + if entity == nil && valueOf != nil { + valueOfElem = valueOf.Elem() + } + + ctLen := len(columnTypes) + // 声明载体数组,用于存放struct的属性指针 + // Declare a carrier array to store the attribute pointer of the struct + values := make([]interface{}, ctLen) + // 记录需要类型转换的字段信息 + var fieldTempDriverValueMap map[*sql.ColumnType]*driverValueInfo + if iscdvm { + fieldTempDriverValueMap = make(map[*sql.ColumnType]*driverValueInfo) + } + var err error + var customDriverValueConver ICustomDriverValueConver + var converOK bool + + for i, columnType := range columnTypes { + if iscdvm { + databaseTypeName := strings.ToUpper(columnType.DatabaseTypeName()) + // 根据接收的类型,获取到类型转换的接口实现,优先匹配指定的数据库类型 + customDriverValueConver, converOK = customDriverValueMap[dialect+"."+databaseTypeName] + if !converOK { + customDriverValueConver, converOK = customDriverValueMap[databaseTypeName] + } + } + dv := driverValue.Index(i) + if dv.IsValid() && dv.InterfaceData()[0] == 0 { // 该字段的数据库值是null,取默认值 + values[i] = new(interface{}) + continue + } else if converOK { // 如果是需要转换的字段 + // 获取字段类型 + var structFieldType *reflect.Type + if entity != nil { // 查询一个字段,并且可以直接接收 + structFieldType = typeOf + } else { // 如果是struct类型 + field, err := getStructFieldByColumnType(columnType, dbColumnFieldMap, exportFieldMap) + if err != nil { + return err + } + if field != nil { // 存在这个字段 + vtype := field.Type + structFieldType = &vtype + } + } + tempDriverValue, err := customDriverValueConver.GetDriverValue(ctx, columnType, structFieldType) + if err != nil { + return err + } + if tempDriverValue == nil { + return errors.New("->sqlRowsValues-->customDriverValueConver.GetDriverValue返回的driver.Value不能为nil") + } + values[i] = tempDriverValue + + // 如果需要类型转换 + dvinfo := driverValueInfo{} + dvinfo.customDriverValueConver = customDriverValueConver + // dvinfo.columnType = columnType + dvinfo.structFieldType = structFieldType + dvinfo.tempDriverValue = tempDriverValue + fieldTempDriverValueMap[columnType] = &dvinfo + continue + + } else if entity != nil { // 查询一个字段,并且可以直接接收 + values[i] = entity + continue + } else { + field, err := getStructFieldByColumnType(columnType, dbColumnFieldMap, exportFieldMap) + if err != nil { + return err + } + if field == nil { // 如果不存在这个字段 + values[i] = new(interface{}) + } else { + // fieldType := refPV.FieldByName(field.Name).Type() + // v := reflect.New(field.Type).Interface() + // 字段的反射值 + fieldValue := valueOfElem.FieldByName(field.Name) + v := fieldValue.Addr().Interface() + // v := new(interface{}) + values[i] = v + } + } + + } + err = rows.Scan(values...) + if err != nil { + return err + } + if len(fieldTempDriverValueMap) < 1 { + return err + } + + // 循环需要替换的值 + for columnType, driverValueInfo := range fieldTempDriverValueMap { + // 根据列名,字段类型,新值 返回符合接收类型值的指针,返回值是个指针,指针,指针!!!! + // typeOf := fieldValue.Type() + rightValue, errConverDriverValue := driverValueInfo.customDriverValueConver.ConverDriverValue(ctx, columnType, driverValueInfo.tempDriverValue, driverValueInfo.structFieldType) + if errConverDriverValue != nil { + errConverDriverValue = fmt.Errorf("->sqlRowsValues-->customDriverValueConver.ConverDriverValue错误:%w", errConverDriverValue) + FuncLogError(ctx, errConverDriverValue) + return errConverDriverValue + } + if entity != nil { // 查询一个字段,并且可以直接接收 + // entity = rightValue + // valueOfElem.Set(reflect.ValueOf(rightValue).Elem()) + reflect.ValueOf(entity).Elem().Set(reflect.ValueOf(rightValue).Elem()) + continue + } else { // 如果是Struct类型接收 + field, err := getStructFieldByColumnType(columnType, dbColumnFieldMap, exportFieldMap) + if err != nil { + return err + } + if field != nil { // 如果存在这个字段 + // 字段的反射值 + fieldValue := valueOfElem.FieldByName(field.Name) + // 给字段赋值 + fieldValue.Set(reflect.ValueOf(rightValue).Elem()) + } + } + + } + + return err +} + +// getStructFieldByColumnType 根据ColumnType获取StructField对象,兼容驼峰 +func getStructFieldByColumnType(columnType *sql.ColumnType, dbColumnFieldMap *map[string]reflect.StructField, exportFieldMap *map[string]reflect.StructField) (*reflect.StructField, error) { + columnName := strings.ToLower(columnType.Name()) + // columnName := "test" + // 从缓存中获取列名的field字段 + // Get the field field of the column name from the cache + field, fok := (*dbColumnFieldMap)[columnName] + if !fok { + field, fok = (*exportFieldMap)[columnName] + if !fok { + // 尝试驼峰 + cname := strings.ReplaceAll(columnName, "_", "") + field, fok = (*exportFieldMap)[cname] + + } + + } + if fok { + return &field, nil + } + return nil, nil +} diff --git a/vendor/gitee.com/chunanyong/zorm/typeConvert.go b/vendor/gitee.com/chunanyong/zorm/typeConvert.go new file mode 100644 index 00000000..2d0aedcb --- /dev/null +++ b/vendor/gitee.com/chunanyong/zorm/typeConvert.go @@ -0,0 +1,611 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zorm + +import ( + "context" + "errors" + "strconv" + + "gitee.com/chunanyong/zorm/decimal" +) + +// FuncDecimalValue 设置decimal类型接收值,复写函数自定义decimal实现,例如github.com/shopspring/decimal,返回的是指针 +var FuncDecimalValue = func(ctx context.Context, dialect string) interface{} { + return &decimal.Decimal{} +} + +// OverrideFunc 重写ZORM的函数,用于风险监控,只要查看这个函数的调用,就知道哪些地方重写了函数,避免项目混乱.当你使用这个函数时,你必须知道自己在做什么 +// funcName 是需要重写的方法命,funcObject是对应的函数. 返回值bool是否重写成功,interface{}是重写前的函数 +// 一般是在init里调用重写 +func OverrideFunc(funcName string, funcObject interface{}) (bool, interface{}, error) { + if funcName == "" { + return false, nil, errors.New("->OverrideFunc-->funcName不能为空") + } + + // oldFunc 老的函数 + var oldFunc interface{} = nil + switch funcName { + case "Transaction": + newFunc, ok := funcObject.(func(ctx context.Context, doTransaction func(ctx context.Context) (interface{}, error)) (interface{}, error)) + if ok { + oldFunc = transaction + transaction = newFunc + } + case "QueryRow": + newFunc, ok := funcObject.(func(ctx context.Context, finder *Finder, entity interface{}) (bool, error)) + if ok { + oldFunc = queryRow + queryRow = newFunc + } + case "Query": + newFunc, ok := funcObject.(func(ctx context.Context, finder *Finder, rowsSlicePtr interface{}, page *Page) error) + if ok { + oldFunc = query + query = newFunc + } + + case "QueryRowMap": + newFunc, ok := funcObject.(func(ctx context.Context, finder *Finder) (map[string]interface{}, error)) + if ok { + oldFunc = queryRowMap + queryRowMap = newFunc + } + case "QueryMap": + newFunc, ok := funcObject.(func(ctx context.Context, finder *Finder, page *Page) ([]map[string]interface{}, error)) + if ok { + oldFunc = queryMap + queryMap = newFunc + } + case "UpdateFinder": + newFunc, ok := funcObject.(func(ctx context.Context, finder *Finder) (int, error)) + if ok { + oldFunc = updateFinder + updateFinder = newFunc + } + case "Insert": + newFunc, ok := funcObject.(func(ctx context.Context, entity IEntityStruct) (int, error)) + if ok { + oldFunc = insert + insert = newFunc + } + case "InsertSlice": + newFunc, ok := funcObject.(func(ctx context.Context, entityStructSlice []IEntityStruct) (int, error)) + if ok { + oldFunc = insertSlice + insertSlice = newFunc + } + case "Update": + newFunc, ok := funcObject.(func(ctx context.Context, entity IEntityStruct) (int, error)) + if ok { + oldFunc = update + update = newFunc + } + case "UpdateNotZeroValue": + newFunc, ok := funcObject.(func(ctx context.Context, entity IEntityStruct) (int, error)) + if ok { + oldFunc = updateNotZeroValue + updateNotZeroValue = newFunc + } + case "Delete": + newFunc, ok := funcObject.(func(ctx context.Context, entity IEntityStruct) (int, error)) + if ok { + oldFunc = delete + delete = newFunc + } + + case "InsertEntityMap": + newFunc, ok := funcObject.(func(ctx context.Context, entity IEntityMap) (int, error)) + if ok { + oldFunc = insertEntityMap + insertEntityMap = newFunc + } + case "InsertEntityMapSlice": + newFunc, ok := funcObject.(func(ctx context.Context, entity []IEntityMap) (int, error)) + if ok { + oldFunc = insertEntityMapSlice + insertEntityMapSlice = newFunc + } + case "UpdateEntityMap": + newFunc, ok := funcObject.(func(ctx context.Context, entity IEntityMap) (int, error)) + if ok { + oldFunc = updateEntityMap + updateEntityMap = newFunc + } + default: + return false, oldFunc, errors.New("->OverrideFunc-->函数" + funcName + "暂不支持重写或不存在") + } + if oldFunc == nil { + return false, oldFunc, errors.New("->OverrideFunc-->请检查传入的" + funcName + "函数实现,断言转换失败.") + } + return true, oldFunc, nil +} + +// typeConvertInt64toInt int64 转 int +func typeConvertInt64toInt(from int64) (int, error) { + strInt64 := strconv.FormatInt(from, 10) + return strconv.Atoi(strInt64) +} + +/* +func typeConvertFloat32(i interface{}) (float32, error) { + if i == nil { + return 0, nil + } + if v, ok := i.(float32); ok { + return v, nil + } + v, err := typeConvertString(i) + if err != nil { + return 0, err + } + vf, err := strconv.ParseFloat(v, 32) + return float32(vf), err +} + +func typeConvertFloat64(i interface{}) (float64, error) { + if i == nil { + return 0, nil + } + if v, ok := i.(float64); ok { + return v, nil + } + v, err := typeConvertString(i) + if err != nil { + return 0, err + } + return strconv.ParseFloat(v, 64) +} + +func typeConvertDecimal(i interface{}) (decimal.Decimal, error) { + if i == nil { + return decimal.Zero, nil + } + if v, ok := i.(decimal.Decimal); ok { + return v, nil + } + v, err := typeConvertString(i) + if err != nil { + return decimal.Zero, err + } + return decimal.NewFromString(v) +} + +func typeConvertInt64(i interface{}) (int64, error) { + if i == nil { + return 0, nil + } + if v, ok := i.(int64); ok { + return v, nil + } + v, err := typeConvertInt(i) + if err != nil { + return 0, err + } + return int64(v), err +} + +func typeConvertString(i interface{}) (string, error) { + if i == nil { + return "", nil + } + switch value := i.(type) { + case int: + return strconv.Itoa(value), nil + case int8: + return strconv.Itoa(int(value)), nil + case int16: + return strconv.Itoa(int(value)), nil + case int32: + return strconv.Itoa(int(value)), nil + case int64: + return strconv.Itoa(int(value)), nil + case uint: + return strconv.FormatUint(uint64(value), 10), nil + case uint8: + return strconv.FormatUint(uint64(value), 10), nil + case uint16: + return strconv.FormatUint(uint64(value), 10), nil + case uint32: + return strconv.FormatUint(uint64(value), 10), nil + case uint64: + return strconv.FormatUint(uint64(value), 10), nil + case float32: + return strconv.FormatFloat(float64(value), 'f', -1, 32), nil + case float64: + return strconv.FormatFloat(value, 'f', -1, 64), nil + case bool: + return strconv.FormatBool(value), nil + case string: + return value, nil + case []byte: + return string(value), nil + default: + return fmt.Sprintf("%v", value), nil + } +} + +//false: "", 0, false, off +func typeConvertBool(i interface{}) (bool, error) { + if i == nil { + return false, nil + } + if v, ok := i.(bool); ok { + return v, nil + } + s, err := typeConvertString(i) + if err != nil { + return false, err + } + if s != "" && s != "0" && s != "false" && s != "off" { + return true, err + } + return false, err +} + +func typeConvertInt(i interface{}) (int, error) { + if i == nil { + return 0, nil + } + switch value := i.(type) { + case int: + return value, nil + case int8: + return int(value), nil + case int16: + return int(value), nil + case int32: + return int(value), nil + case int64: + return int(value), nil + case uint: + return int(value), nil + case uint8: + return int(value), nil + case uint16: + return int(value), nil + case uint32: + return int(value), nil + case uint64: + return int(value), nil + case float32: + return int(value), nil + case float64: + return int(value), nil + case bool: + if value { + return 1, nil + } + return 0, nil + default: + v, err := typeConvertString(value) + if err != nil { + return 0, err + } + return strconv.Atoi(v) + } +} + + + +func typeConvertTime(i interface{}, format string, TZLocation ...*time.Location) (time.Time, error) { + s, err := typeConvertString(i) + if err != nil { + return time.Time{}, err + } + return typeConvertStrToTime(s, format, TZLocation...) +} + +func typeConvertStrToTime(str string, format string, TZLocation ...*time.Location) (time.Time, error) { + if len(TZLocation) > 0 { + return time.ParseInLocation(format, str, TZLocation[0]) + } + return time.ParseInLocation(format, str, time.Local) +} + +func encodeString(s string) []byte { + return []byte(s) +} + +func decodeToString(b []byte) string { + return string(b) +} + +func encodeBool(b bool) []byte { + if b { + return []byte{1} + } + return []byte{0} + +} + +func encodeInt(i int) []byte { + if i <= math.MaxInt8 { + return encodeInt8(int8(i)) + } else if i <= math.MaxInt16 { + return encodeInt16(int16(i)) + } else if i <= math.MaxInt32 { + return encodeInt32(int32(i)) + } else { + return encodeInt64(int64(i)) + } +} + +func encodeUint(i uint) []byte { + if i <= math.MaxUint8 { + return encodeUint8(uint8(i)) + } else if i <= math.MaxUint16 { + return encodeUint16(uint16(i)) + } else if i <= math.MaxUint32 { + return encodeUint32(uint32(i)) + } else { + return encodeUint64(uint64(i)) + } +} + +func encodeInt8(i int8) []byte { + return []byte{byte(i)} +} + +func encodeUint8(i uint8) []byte { + return []byte{byte(i)} +} + +func encodeInt16(i int16) []byte { + bytes := make([]byte, 2) + binary.LittleEndian.PutUint16(bytes, uint16(i)) + return bytes +} + +func encodeUint16(i uint16) []byte { + bytes := make([]byte, 2) + binary.LittleEndian.PutUint16(bytes, i) + return bytes +} + +func encodeInt32(i int32) []byte { + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, uint32(i)) + return bytes +} + +func encodeUint32(i uint32) []byte { + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, i) + return bytes +} + +func encodeInt64(i int64) []byte { + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, uint64(i)) + return bytes +} + +func encodeUint64(i uint64) []byte { + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, i) + return bytes +} + +func encodeFloat32(f float32) []byte { + bits := math.Float32bits(f) + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, bits) + return bytes +} + +func encodeFloat64(f float64) []byte { + bits := math.Float64bits(f) + bytes := make([]byte, 8) + binary.LittleEndian.PutUint64(bytes, bits) + return bytes +} + +func encode(vs ...interface{}) []byte { + buf := new(bytes.Buffer) + for i := 0; i < len(vs); i++ { + switch value := vs[i].(type) { + case int: + buf.Write(encodeInt(value)) + case int8: + buf.Write(encodeInt8(value)) + case int16: + buf.Write(encodeInt16(value)) + case int32: + buf.Write(encodeInt32(value)) + case int64: + buf.Write(encodeInt64(value)) + case uint: + buf.Write(encodeUint(value)) + case uint8: + buf.Write(encodeUint8(value)) + case uint16: + buf.Write(encodeUint16(value)) + case uint32: + buf.Write(encodeUint32(value)) + case uint64: + buf.Write(encodeUint64(value)) + case bool: + buf.Write(encodeBool(value)) + case string: + buf.Write(encodeString(value)) + case []byte: + buf.Write(value) + case float32: + buf.Write(encodeFloat32(value)) + case float64: + buf.Write(encodeFloat64(value)) + default: + if err := binary.Write(buf, binary.LittleEndian, value); err != nil { + buf.Write(encodeString(fmt.Sprintf("%v", value))) + } + } + } + return buf.Bytes() +} + +func isNumeric(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < byte('0') || s[i] > byte('9') { + return false + } + } + return true +} +func typeConvertTimeDuration(i interface{}) time.Duration { + return time.Duration(typeConvertInt64(i)) +} + +func typeConvertBytes(i interface{}) []byte { + if i == nil { + return nil + } + if r, ok := i.([]byte); ok { + return r + } + return encode(i) + +} + +func typeConvertStrings(i interface{}) []string { + if i == nil { + return nil + } + if r, ok := i.([]string); ok { + return r + } else if r, ok := i.([]interface{}); ok { + strs := make([]string, len(r)) + for k, v := range r { + strs[k] = typeConvertString(v) + } + return strs + } + return []string{fmt.Sprintf("%v", i)} +} + +func typeConvertInt8(i interface{}) int8 { + if i == nil { + return 0 + } + if v, ok := i.(int8); ok { + return v + } + return int8(typeConvertInt(i)) +} + +func typeConvertInt16(i interface{}) int16 { + if i == nil { + return 0 + } + if v, ok := i.(int16); ok { + return v + } + return int16(typeConvertInt(i)) +} + +func typeConvertInt32(i interface{}) int32 { + if i == nil { + return 0 + } + if v, ok := i.(int32); ok { + return v + } + return int32(typeConvertInt(i)) +} + +func typeConvertUint(i interface{}) uint { + if i == nil { + return 0 + } + switch value := i.(type) { + case int: + return uint(value) + case int8: + return uint(value) + case int16: + return uint(value) + case int32: + return uint(value) + case int64: + return uint(value) + case uint: + return value + case uint8: + return uint(value) + case uint16: + return uint(value) + case uint32: + return uint(value) + case uint64: + return uint(value) + case float32: + return uint(value) + case float64: + return uint(value) + case bool: + if value { + return 1 + } + return 0 + default: + v, _ := strconv.ParseUint(typeConvertString(value), 10, 64) + return uint(v) + } +} + +func typeConvertUint8(i interface{}) uint8 { + if i == nil { + return 0 + } + if v, ok := i.(uint8); ok { + return v + } + return uint8(typeConvertUint(i)) +} + +func typeConvertUint16(i interface{}) uint16 { + if i == nil { + return 0 + } + if v, ok := i.(uint16); ok { + return v + } + return uint16(typeConvertUint(i)) +} + +func typeConvertUint32(i interface{}) uint32 { + if i == nil { + return 0 + } + if v, ok := i.(uint32); ok { + return v + } + return uint32(typeConvertUint(i)) +} + +func typeConvertUint64(i interface{}) uint64 { + if i == nil { + return 0 + } + if v, ok := i.(uint64); ok { + return v + } + return uint64(typeConvertUint(i)) +} +*/ diff --git a/vendor/gitee.com/chunanyong/zorm/zorm-logo.png b/vendor/gitee.com/chunanyong/zorm/zorm-logo.png new file mode 100644 index 00000000..00eb5278 Binary files /dev/null and b/vendor/gitee.com/chunanyong/zorm/zorm-logo.png differ diff --git a/vendor/github.com/bmizerany/pq/.gitignore b/vendor/github.com/bmizerany/pq/.gitignore new file mode 100644 index 00000000..0f1d00e1 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/.gitignore @@ -0,0 +1,4 @@ +.db +*.test +*~ +*.swp diff --git a/vendor/github.com/bmizerany/pq/LICENSE.md b/vendor/github.com/bmizerany/pq/LICENSE.md new file mode 100644 index 00000000..258bdff0 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (C) 2011 Blake Mizerany + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/bmizerany/pq/README.md b/vendor/github.com/bmizerany/pq/README.md new file mode 100644 index 00000000..dd7a6f59 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/README.md @@ -0,0 +1,99 @@ +# pq - A pure Go postgres driver for Go's database/sql package + +**This package is now deprecated. The up to date version is at +[github.com/lib/pq](https://github.com/lib/pq).** + +## Install + + go get github.com/bmizerany/pq + +## Docs + + + +## Use + + package main + + import ( + _ "github.com/bmizerany/pq" + "database/sql" + ) + + func main() { + db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") + // ... + } + +**Connection String Parameters** + +These are a subset of the libpq connection parameters. In addition, a +number of the [environment +variables](http://www.postgresql.org/docs/9.1/static/libpq-envars.html) +supported by libpq are also supported. Just like libpq, these have +lower precedence than explicitly provided connection parameters. + +See http://www.postgresql.org/docs/9.1/static/libpq-connect.html. + +* `dbname` - The name of the database to connect to +* `user` - The user to sign in as +* `password` - The user's password +* `host` - The host to connect to. Values that start with `/` are for unix domain sockets. (default is `localhost`) +* `port` - The port to bind to. (default is `5432`) +* `sslmode` - Whether or not to use SSL (default is `require`, this is not the default for libpq) + Valid values are: + * `disable` - No SSL + * `require` - Always SSL (skip verification) + * `verify-full` - Always SSL (require verification) + +See http://golang.org/pkg/database/sql to learn how to use with `pq` through the `database/sql` package. + +## Tests + +`go test` is used for testing. A running PostgreSQL server is +required, with the ability to log in. The default database to connect +to test with is "pqgotest," but it can be overridden using environment +variables. + +Example: + + PGHOST=/var/run/postgresql go test pq + +## Features + +* SSL +* Handles bad connections for `database/sql` +* Scan `time.Time` correctly (i.e. `timestamp[tz]`, `time[tz]`, `date`) +* Scan binary blobs correctly (i.e. `bytea`) +* pq.ParseURL for converting urls to connection strings for sql.Open. +* Many libpq compatible environment variables +* Unix socket support + +## Future / Things you can help with + +* Notifications: `LISTEN`/`NOTIFY` +* `hstore` sugar (i.e. handling hstore in `rows.Scan`) + +## Thank you (alphabetical) + +Some of these contributors are from the original library `bmizerany/pq.go` whose +code still exists in here. + +* Andy Balholm (andybalholm) +* Ben Berkert (benburkert) +* Bjørn Madsen (aeons) +* Blake Gentry (bgentry) +* Brad Fitzpatrick (bradfitz) +* Daniel Farina (fdr) +* Everyone at The Go Team +* Federico Romero (federomero) +* Heroku (heroku) +* John Gallagher (jgallagher) +* Kamil Kisiel (kisielk) +* Keith Rarick (kr) +* Marc Brinkmann (mbr) +* Martin Olsen (martinolsen) +* Mike Lewis (mikelikespie) +* Ryan Smith (ryandotsmith) +* Samuel Stauffer (samuel) +* notedit (notedit) diff --git a/vendor/github.com/bmizerany/pq/buf.go b/vendor/github.com/bmizerany/pq/buf.go new file mode 100644 index 00000000..cb4f5493 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/buf.go @@ -0,0 +1,80 @@ +package pq + +import ( + "bytes" + "encoding/binary" +) + +type readBuf []byte + +func (b *readBuf) int32() (n int) { + n = int(int32(binary.BigEndian.Uint32(*b))) + *b = (*b)[4:] + return +} + +func (b *readBuf) oid() (n oid) { + n = oid(binary.BigEndian.Uint32(*b)) + *b = (*b)[4:] + return +} + +func (b *readBuf) int16() (n int) { + n = int(binary.BigEndian.Uint16(*b)) + *b = (*b)[2:] + return +} + +var stringTerm = []byte{0} + +func (b *readBuf) string() string { + i := bytes.Index(*b, stringTerm) + if i < 0 { + errorf("invalid message format; expected string terminator") + } + s := (*b)[:i] + *b = (*b)[i+1:] + return string(s) +} + +func (b *readBuf) next(n int) (v []byte) { + v = (*b)[:n] + *b = (*b)[n:] + return +} + +func (b *readBuf) byte() byte { + return b.next(1)[0] +} + +type writeBuf []byte + +func newWriteBuf(c byte) *writeBuf { + b := make(writeBuf, 5) + b[0] = c + return &b +} + +func (b *writeBuf) int32(n int) { + x := make([]byte, 4) + binary.BigEndian.PutUint32(x, uint32(n)) + *b = append(*b, x...) +} + +func (b *writeBuf) int16(n int) { + x := make([]byte, 2) + binary.BigEndian.PutUint16(x, uint16(n)) + *b = append(*b, x...) +} + +func (b *writeBuf) string(s string) { + *b = append(*b, (s + "\000")...) +} + +func (b *writeBuf) byte(c byte) { + *b = append(*b, c) +} + +func (b *writeBuf) bytes(v []byte) { + *b = append(*b, v...) +} diff --git a/vendor/github.com/bmizerany/pq/conn.go b/vendor/github.com/bmizerany/pq/conn.go new file mode 100644 index 00000000..79ce2c1b --- /dev/null +++ b/vendor/github.com/bmizerany/pq/conn.go @@ -0,0 +1,678 @@ +package pq + +import ( + "bufio" + "crypto/md5" + "crypto/tls" + "database/sql" + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" + "os/user" + "path" + "strconv" + "strings" +) + +var ( + ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server") + ErrNotSupported = errors.New("pq: invalid command") +) + +type drv struct{} + +func (d *drv) Open(name string) (driver.Conn, error) { + return Open(name) +} + +func init() { + sql.Register("postgres", &drv{}) +} + +type conn struct { + c net.Conn + buf *bufio.Reader + namei int +} + +func Open(name string) (_ driver.Conn, err error) { + defer errRecover(&err) + defer errRecoverWithPGReason(&err) + + o := make(Values) + + // A number of defaults are applied here, in this order: + // + // * Very low precedence defaults applied in every situation + // * Environment variables + // * Explicitly passed connection information + o.Set("host", "localhost") + o.Set("port", "5432") + + // Default the username, but ignore errors, because a user + // passed in via environment variable or connection string + // would be okay. This can result in connections failing + // *sometimes* if the client relies on being able to determine + // the current username and there are intermittent problems. + u, err := user.Current() + if err == nil { + o.Set("user", u.Username) + } + + for k, v := range parseEnviron(os.Environ()) { + o.Set(k, v) + } + + parseOpts(name, o) + + c, err := net.Dial(network(o)) + if err != nil { + return nil, err + } + + cn := &conn{c: c} + cn.ssl(o) + cn.buf = bufio.NewReader(cn.c) + cn.startup(o) + return cn, nil +} + +func network(o Values) (string, string) { + host := o.Get("host") + + if strings.HasPrefix(host, "/") { + sockPath := path.Join(host, ".s.PGSQL."+o.Get("port")) + return "unix", sockPath + } + + return "tcp", host + ":" + o.Get("port") +} + +type Values map[string]string + +func (vs Values) Set(k, v string) { + vs[k] = v +} + +func (vs Values) Get(k string) (v string) { + v, _ = vs[k] + return +} + +func parseOpts(name string, o Values) { + if len(name) == 0 { + return + } + + ps := strings.Split(name, " ") + for _, p := range ps { + kv := strings.Split(p, "=") + if len(kv) < 2 { + errorf("invalid option: %q", p) + } + o.Set(kv[0], kv[1]) + } +} + +func (cn *conn) Begin() (driver.Tx, error) { + _, err := cn.Exec("BEGIN", nil) + if err != nil { + return nil, err + } + return cn, err +} + +func (cn *conn) Commit() error { + _, err := cn.Exec("COMMIT", nil) + return err +} + +func (cn *conn) Rollback() error { + _, err := cn.Exec("ROLLBACK", nil) + return err +} + +func (cn *conn) gname() string { + cn.namei++ + return strconv.FormatInt(int64(cn.namei), 10) +} + +func (cn *conn) simpleQuery(q string) (res driver.Result, err error) { + defer errRecover(&err) + + b := newWriteBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C': + res = parseComplete(r.string()) + case 'Z': + // done + return + case 'E': + err = parseError(r) + case 'T', 'N', 'S': + // ignore + default: + errorf("unknown response for simple query: %q", t) + } + } + panic("not reached") +} + +func (cn *conn) prepareTo(q, stmtName string) (_ driver.Stmt, err error) { + defer errRecover(&err) + + st := &stmt{cn: cn, name: stmtName, query: q} + + b := newWriteBuf('P') + b.string(st.name) + b.string(q) + b.int16(0) + cn.send(b) + + b = newWriteBuf('D') + b.byte('S') + b.string(st.name) + cn.send(b) + + cn.send(newWriteBuf('S')) + + for { + t, r := cn.recv1() + switch t { + case '1', '2', 'N': + case 't': + st.nparams = int(r.int16()) + st.paramTyps = make([]oid, st.nparams, st.nparams) + + for i := 0; i < st.nparams; i += 1 { + st.paramTyps[i] = r.oid() + } + case 'T': + n := r.int16() + st.cols = make([]string, n) + st.rowTyps = make([]oid, n) + for i := range st.cols { + st.cols[i] = r.string() + r.next(6) + st.rowTyps[i] = r.oid() + r.next(8) + } + case 'n': + // no data + case 'Z': + return st, err + case 'E': + err = parseError(r) + default: + errorf("unexpected describe rows response: %q", t) + } + } + + panic("not reached") +} + +func (cn *conn) Prepare(q string) (driver.Stmt, error) { + return cn.prepareTo(q, cn.gname()) +} + +func (cn *conn) Close() (err error) { + defer errRecover(&err) + cn.send(newWriteBuf('X')) + + return cn.c.Close() +} + +// Implement the optional "Execer" interface for one-shot queries +func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err error) { + defer errRecover(&err) + + // Check to see if we can use the "simpleQuery" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + return cn.simpleQuery(query) + } + + // Use the unnamed statement to defer planning until bind + // time, or else value-based selectivity estimates cannot be + // used. + st, err := cn.prepareTo(query, "") + if err != nil { + panic(err) + } + + r, err := st.Exec(args) + if err != nil { + panic(err) + } + + return r, err +} + +// Assumes len(*m) is > 5 +func (cn *conn) send(m *writeBuf) { + b := (*m)[1:] + binary.BigEndian.PutUint32(b, uint32(len(b))) + + if (*m)[0] == 0 { + *m = b + } + + _, err := cn.c.Write(*m) + if err != nil { + panic(err) + } +} + +func (cn *conn) recv() (t byte, r *readBuf) { + for { + t, r = cn.recv1() + switch t { + case 'E': + panic(parseError(r)) + case 'N': + // ignore + default: + return + } + } + + panic("not reached") +} + +func (cn *conn) recv1() (byte, *readBuf) { + x := make([]byte, 5) + _, err := io.ReadFull(cn.buf, x) + if err != nil { + panic(err) + } + + b := readBuf(x[1:]) + y := make([]byte, b.int32()-4) + _, err = io.ReadFull(cn.buf, y) + if err != nil { + panic(err) + } + + return x[0], (*readBuf)(&y) +} + +func (cn *conn) ssl(o Values) { + tlsConf := tls.Config{} + switch mode := o.Get("sslmode"); mode { + case "require", "": + tlsConf.InsecureSkipVerify = true + case "verify-full": + // fall out + case "disable": + return + default: + errorf(`unsupported sslmode %q; only "require" (default), "verify-full", and "disable" supported`, mode) + } + + w := newWriteBuf(0) + w.int32(80877103) + cn.send(w) + + b := make([]byte, 1) + _, err := io.ReadFull(cn.c, b) + if err != nil { + panic(err) + } + + if b[0] != 'S' { + panic(ErrSSLNotSupported) + } + + cn.c = tls.Client(cn.c, &tlsConf) +} + +func (cn *conn) startup(o Values) { + w := newWriteBuf(0) + w.int32(196608) + w.string("user") + w.string(o.Get("user")) + w.string("database") + w.string(o.Get("dbname")) + w.string("") + cn.send(w) + + for { + t, r := cn.recv() + switch t { + case 'K', 'S': + case 'R': + cn.auth(r, o) + case 'Z': + return + default: + errorf("unknown response for startup: %q", t) + } + } +} + +func (cn *conn) auth(r *readBuf, o Values) { + switch code := r.int32(); code { + case 0: + // OK + case 3: + w := newWriteBuf('p') + w.string(o.Get("password")) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + case 5: + s := string(r.next(4)) + w := newWriteBuf('p') + w.string("md5" + md5s(md5s(o.Get("password")+o.Get("user"))+s)) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication resoonse: %q", t) + } + default: + errorf("unknown authentication response: %d", code) + } +} + +type stmt struct { + cn *conn + name string + query string + cols []string + nparams int + rowTyps []oid + paramTyps []oid + closed bool +} + +func (st *stmt) Close() (err error) { + if st.closed { + return nil + } + + defer errRecover(&err) + + w := newWriteBuf('C') + w.byte('S') + w.string(st.name) + st.cn.send(w) + + st.cn.send(newWriteBuf('S')) + + t, _ := st.cn.recv() + if t != '3' { + errorf("unexpected close response: %q", t) + } + st.closed = true + + t, _ = st.cn.recv() + if t != 'Z' { + errorf("expected ready for query, but got: %q", t) + } + + return nil +} + +func (st *stmt) Query(v []driver.Value) (_ driver.Rows, err error) { + defer errRecover(&err) + st.exec(v) + return &rows{st: st}, nil +} + +func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { + defer errRecover(&err) + + if len(v) == 0 { + return st.cn.simpleQuery(st.query) + } + st.exec(v) + + for { + t, r := st.cn.recv1() + switch t { + case 'E': + err = parseError(r) + case 'C': + res = parseComplete(r.string()) + case 'Z': + // done + return + case 'D': + errorf("unexpected data row returned in Exec; check your query") + case 'S', 'N': + // Ignore + default: + errorf("unknown exec response: %q", t) + } + } + + panic("not reached") +} + +func (st *stmt) exec(v []driver.Value) { + w := newWriteBuf('B') + w.string("") + w.string(st.name) + w.int16(0) + w.int16(len(v)) + for i, x := range v { + if x == nil { + w.int32(-1) + } else { + b := encode(x, st.paramTyps[i]) + w.int32(len(b)) + w.bytes(b) + } + } + w.int16(0) + st.cn.send(w) + + w = newWriteBuf('E') + w.string("") + w.int32(0) + st.cn.send(w) + + st.cn.send(newWriteBuf('S')) + + var err error + for { + t, r := st.cn.recv1() + switch t { + case 'E': + err = parseError(r) + case '2': + if err != nil { + panic(err) + } + return + case 'Z': + if err != nil { + panic(err) + } + return + case 'N': + // ignore + default: + errorf("unexpected bind response: %q", t) + } + } +} + +func (st *stmt) NumInput() int { + return st.nparams +} + +type result int64 + +func (i result) RowsAffected() (int64, error) { + return int64(i), nil +} + +func (i result) LastInsertId() (int64, error) { + return 0, ErrNotSupported +} + +func parseComplete(s string) driver.Result { + parts := strings.Split(s, " ") + n, _ := strconv.ParseInt(parts[len(parts)-1], 10, 64) + return result(n) +} + +type rows struct { + st *stmt + done bool +} + +func (rs *rows) Close() error { + for { + err := rs.Next(nil) + switch err { + case nil: + case io.EOF: + return nil + default: + return err + } + } + panic("not reached") +} + +func (rs *rows) Columns() []string { + return rs.st.cols +} + +func (rs *rows) Next(dest []driver.Value) (err error) { + if rs.done { + return io.EOF + } + + defer errRecover(&err) + + for { + t, r := rs.st.cn.recv1() + switch t { + case 'E': + err = parseError(r) + case 'C', 'S', 'N': + continue + case 'Z': + rs.done = true + if err != nil { + return err + } + return io.EOF + case 'D': + n := r.int16() + for i := 0; i < len(dest) && i < n; i++ { + l := r.int32() + if l == -1 { + dest[i] = nil + continue + } + dest[i] = decode(r.next(l), rs.st.rowTyps[i]) + } + return + default: + errorf("unexpected message after execute: %q", t) + } + } + + panic("not reached") +} + +func md5s(s string) string { + h := md5.New() + h.Write([]byte(s)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// parseEnviron tries to mimic some of libpq's environment handling +// +// To ease testing, it does not directly reference os.Environ, but is +// designed to accept its output. +// +// Environment-set connection information is intended to have a higher +// precedence than a library default but lower than any explicitly +// passed information (such as in the URL or connection string). +func parseEnviron(env []string) (out map[string]string) { + out = make(map[string]string) + + for _, v := range env { + parts := strings.SplitN(v, "=", 2) + + accrue := func(keyname string) { + out[keyname] = parts[1] + } + + // The order of these is the same as is seen in the + // PostgreSQL 9.1 manual, with omissions briefly + // noted. + switch parts[0] { + case "PGHOST": + accrue("host") + case "PGHOSTADDR": + accrue("hostaddr") + case "PGPORT": + accrue("port") + case "PGDATABASE": + accrue("dbname") + case "PGUSER": + accrue("user") + case "PGPASSWORD": + accrue("password") + // skip PGPASSFILE, PGSERVICE, PGSERVICEFILE, + // PGREALM + case "PGOPTIONS": + accrue("options") + case "PGAPPNAME": + accrue("application_name") + case "PGSSLMODE": + accrue("sslmode") + case "PGREQUIRESSL": + accrue("requiressl") + case "PGSSLCERT": + accrue("sslcert") + case "PGSSLKEY": + accrue("sslkey") + case "PGSSLROOTCERT": + accrue("sslrootcert") + case "PGSSLCRL": + accrue("sslcrl") + case "PGREQUIREPEER": + accrue("requirepeer") + case "PGKRBSRVNAME": + accrue("krbsrvname") + case "PGGSSLIB": + accrue("gsslib") + case "PGCONNECT_TIMEOUT": + accrue("connect_timeout") + case "PGCLIENTENCODING": + accrue("client_encoding") + // skip PGDATESTYLE, PGTZ, PGGEQO, PGSYSCONFDIR, + // PGLOCALEDIR + } + } + + return out +} diff --git a/vendor/github.com/bmizerany/pq/encode.go b/vendor/github.com/bmizerany/pq/encode.go new file mode 100644 index 00000000..819e9457 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/encode.go @@ -0,0 +1,121 @@ +package pq + +import ( + "database/sql/driver" + "encoding/hex" + "fmt" + "strconv" + "time" +) + +func encode(x interface{}, pgtypoid oid) []byte { + switch v := x.(type) { + case int64: + return []byte(fmt.Sprintf("%d", v)) + case float32, float64: + return []byte(fmt.Sprintf("%f", v)) + case []byte: + if pgtypoid == t_bytea { + return []byte(fmt.Sprintf("\\x%x", v)) + } + + return v + case string: + if pgtypoid == t_bytea { + return []byte(fmt.Sprintf("\\x%x", v)) + } + + return []byte(v) + case bool: + return []byte(fmt.Sprintf("%t", v)) + case time.Time: + return []byte(v.Format(time.RFC3339Nano)) + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func decode(s []byte, typ oid) interface{} { + switch typ { + case t_bytea: + s = s[2:] // trim off "\\x" + d := make([]byte, hex.DecodedLen(len(s))) + _, err := hex.Decode(d, s) + if err != nil { + errorf("%s", err) + } + return d + case t_timestamptz: + return mustParse("2006-01-02 15:04:05-07", typ, s) + case t_timestamp: + return mustParse("2006-01-02 15:04:05", typ, s) + case t_time: + return mustParse("15:04:05", typ, s) + case t_timetz: + return mustParse("15:04:05-07", typ, s) + case t_date: + return mustParse("2006-01-02", typ, s) + case t_bool: + return s[0] == 't' + case t_int8, t_int2, t_int4: + i, err := strconv.ParseInt(string(s), 10, 64) + if err != nil { + errorf("%s", err) + } + return i + case t_float4, t_float8: + bits := 64 + if typ == t_float4 { + bits = 32 + } + f, err := strconv.ParseFloat(string(s), bits) + if err != nil { + errorf("%s", err) + } + return f + } + + return s +} + +func mustParse(f string, typ oid, s []byte) time.Time { + str := string(s) + + // Special case until time.Parse bug is fixed: + // http://code.google.com/p/go/issues/detail?id=3487 + if str[len(str)-2] == '.' { + str += "0" + } + + // check for a 30-minute-offset timezone + if (typ == t_timestamptz || typ == t_timetz) && + str[len(str)-3] == ':' { + f += ":00" + } + t, err := time.Parse(f, str) + if err != nil { + errorf("decode: %s", err) + } + return t +} + +type NullTime struct { + Time time.Time + Valid bool // Valid is true if Time is not NULL +} + +// Scan implements the Scanner interface. +func (nt *NullTime) Scan(value interface{}) error { + nt.Time, nt.Valid = value.(time.Time) + return nil +} + +// Value implements the driver Valuer interface. +func (nt NullTime) Value() (driver.Value, error) { + if !nt.Valid { + return nil, nil + } + return nt.Time, nil +} diff --git a/vendor/github.com/bmizerany/pq/error.go b/vendor/github.com/bmizerany/pq/error.go new file mode 100644 index 00000000..9384ab3e --- /dev/null +++ b/vendor/github.com/bmizerany/pq/error.go @@ -0,0 +1,108 @@ +package pq + +import ( + "database/sql/driver" + "fmt" + "io" + "net" + "runtime" +) + +const ( + Efatal = "FATAL" + Epanic = "PANIC" + Ewarning = "WARNING" + Enotice = "NOTICE" + Edebug = "DEBUG" + Einfo = "INFO" + Elog = "LOG" +) + +type Error error + +type PGError interface { + Error() string + Fatal() bool + Get(k byte) (v string) +} +type pgError struct { + c map[byte]string +} + +func parseError(r *readBuf) *pgError { + err := &pgError{make(map[byte]string)} + for t := r.byte(); t != 0; t = r.byte() { + err.c[t] = r.string() + } + return err +} + +func (err *pgError) Get(k byte) (v string) { + v, _ = err.c[k] + return +} + +func (err *pgError) Fatal() bool { + return err.Get('S') == Efatal +} + +func (err *pgError) Error() string { + var s string + for k, v := range err.c { + s += fmt.Sprintf(" %c:%q", k, v) + } + return "pq: " + s[1:] +} + +func errorf(s string, args ...interface{}) { + panic(Error(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)))) +} + +type SimplePGError struct { + pgError +} + +func (err *SimplePGError) Error() string { + return "pq: " + err.Get('M') +} + +func errRecoverWithPGReason(err *error) { + e := recover() + switch v := e.(type) { + case nil: + // Do nothing + case *pgError: + // Return a SimplePGError in place + *err = &SimplePGError{*v} + default: + // Otherwise re-panic + panic(e) + } +} + +func errRecover(err *error) { + e := recover() + switch v := e.(type) { + case nil: + // Do nothing + case runtime.Error: + panic(v) + case *pgError: + if v.Fatal() { + *err = driver.ErrBadConn + } else { + *err = v + } + case *net.OpError: + *err = driver.ErrBadConn + case error: + if v == io.EOF || v.(error).Error() == "remote error: handshake failure" { + *err = driver.ErrBadConn + } else { + *err = v + } + + default: + panic(fmt.Sprintf("unknown error: %#v", e)) + } +} diff --git a/vendor/github.com/bmizerany/pq/types.go b/vendor/github.com/bmizerany/pq/types.go new file mode 100644 index 00000000..7d069644 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/types.go @@ -0,0 +1,319 @@ +package pq + +type oid uint32 + +const ( + t_bool oid = 16 + t_bytea = 17 + t_char = 18 + t_name = 19 + t_int8 = 20 + t_int2 = 21 + t_int2vector = 22 + t_int4 = 23 + t_regproc = 24 + t_text = 25 + t_oid = 26 + t_tid = 27 + t_xid = 28 + t_cid = 29 + t_oidvector = 30 + t_pg_type = 71 + t_pg_attribute = 75 + t_pg_proc = 81 + t_pg_class = 83 + t_xml = 142 + t__xml = 143 + t_pg_node_tree = 194 + t_smgr = 210 + t_point = 600 + t_lseg = 601 + t_path = 602 + t_box = 603 + t_polygon = 604 + t_line = 628 + t__line = 629 + t_float4 = 700 + t_float8 = 701 + t_abstime = 702 + t_reltime = 703 + t_tinterval = 704 + t_unknown = 705 + t_circle = 718 + t__circle = 719 + t_money = 790 + t__money = 791 + t_macaddr = 829 + t_inet = 869 + t_cidr = 650 + t__bool = 1000 + t__bytea = 1001 + t__char = 1002 + t__name = 1003 + t__int2 = 1005 + t__int2vector = 1006 + t__int4 = 1007 + t__regproc = 1008 + t__text = 1009 + t__oid = 1028 + t__tid = 1010 + t__xid = 1011 + t__cid = 1012 + t__oidvector = 1013 + t__bpchar = 1014 + t__varchar = 1015 + t__int8 = 1016 + t__point = 1017 + t__lseg = 1018 + t__path = 1019 + t__box = 1020 + t__float4 = 1021 + t__float8 = 1022 + t__abstime = 1023 + t__reltime = 1024 + t__tinterval = 1025 + t__polygon = 1027 + t_aclitem = 1033 + t__aclitem = 1034 + t__macaddr = 1040 + t__inet = 1041 + t__cidr = 651 + t__cstring = 1263 + t_bpchar = 1042 + t_varchar = 1043 + t_date = 1082 + t_time = 1083 + t_timestamp = 1114 + t__timestamp = 1115 + t__date = 1182 + t__time = 1183 + t_timestamptz = 1184 + t__timestamptz = 1185 + t_interval = 1186 + t__interval = 1187 + t__numeric = 1231 + t_timetz = 1266 + t__timetz = 1270 + t_bit = 1560 + t__bit = 1561 + t_varbit = 1562 + t__varbit = 1563 + t_numeric = 1700 + t_refcursor = 1790 + t__refcursor = 2201 + t_regprocedure = 2202 + t_regoper = 2203 + t_regoperator = 2204 + t_regclass = 2205 + t_regtype = 2206 + t__regprocedure = 2207 + t__regoper = 2208 + t__regoperator = 2209 + t__regclass = 2210 + t__regtype = 2211 + t_uuid = 2950 + t__uuid = 2951 + t_tsvector = 3614 + t_gtsvector = 3642 + t_tsquery = 3615 + t_regconfig = 3734 + t_regdictionary = 3769 + t__tsvector = 3643 + t__gtsvector = 3644 + t__tsquery = 3645 + t__regconfig = 3735 + t__regdictionary = 3770 + t_txid_snapshot = 2970 + t__txid_snapshot = 2949 + t_record = 2249 + t__record = 2287 + t_cstring = 2275 + t_any = 2276 + t_anyarray = 2277 + t_void = 2278 + t_trigger = 2279 + t_language_handler = 2280 + t_internal = 2281 + t_opaque = 2282 + t_anyelement = 2283 + t_anynonarray = 2776 + t_anyenum = 3500 + t_fdw_handler = 3115 + t_pg_attrdef = 10000 + t_pg_constraint = 10001 + t_pg_inherits = 10002 + t_pg_index = 10003 + t_pg_operator = 10004 + t_pg_opfamily = 10005 + t_pg_opclass = 10006 + t_pg_am = 10117 + t_pg_amop = 10118 + t_pg_amproc = 10478 + t_pg_language = 10731 + t_pg_largeobject_metadata = 10732 + t_pg_largeobject = 10733 + t_pg_aggregate = 10734 + t_pg_statistic = 10735 + t_pg_rewrite = 10736 + t_pg_trigger = 10737 + t_pg_description = 10738 + t_pg_cast = 10739 + t_pg_enum = 10936 + t_pg_namespace = 10937 + t_pg_conversion = 10938 + t_pg_depend = 10939 + t_pg_database = 1248 + t_pg_db_role_setting = 10940 + t_pg_tablespace = 10941 + t_pg_pltemplate = 10942 + t_pg_authid = 2842 + t_pg_auth_members = 2843 + t_pg_shdepend = 10943 + t_pg_shdescription = 10944 + t_pg_ts_config = 10945 + t_pg_ts_config_map = 10946 + t_pg_ts_dict = 10947 + t_pg_ts_parser = 10948 + t_pg_ts_template = 10949 + t_pg_extension = 10950 + t_pg_foreign_data_wrapper = 10951 + t_pg_foreign_server = 10952 + t_pg_user_mapping = 10953 + t_pg_foreign_table = 10954 + t_pg_default_acl = 10955 + t_pg_seclabel = 10956 + t_pg_collation = 10957 + t_pg_toast_2604 = 10958 + t_pg_toast_2606 = 10959 + t_pg_toast_2609 = 10960 + t_pg_toast_1255 = 10961 + t_pg_toast_2618 = 10962 + t_pg_toast_3596 = 10963 + t_pg_toast_2619 = 10964 + t_pg_toast_2620 = 10965 + t_pg_toast_1262 = 10966 + t_pg_toast_2396 = 10967 + t_pg_toast_2964 = 10968 + t_pg_roles = 10970 + t_pg_shadow = 10973 + t_pg_group = 10976 + t_pg_user = 10979 + t_pg_rules = 10982 + t_pg_views = 10986 + t_pg_tables = 10989 + t_pg_indexes = 10993 + t_pg_stats = 10997 + t_pg_locks = 11001 + t_pg_cursors = 11004 + t_pg_available_extensions = 11007 + t_pg_available_extension_versions = 11010 + t_pg_prepared_xacts = 11013 + t_pg_prepared_statements = 11017 + t_pg_seclabels = 11020 + t_pg_settings = 11024 + t_pg_timezone_abbrevs = 11029 + t_pg_timezone_names = 11032 + t_pg_stat_all_tables = 11035 + t_pg_stat_xact_all_tables = 11039 + t_pg_stat_sys_tables = 11043 + t_pg_stat_xact_sys_tables = 11047 + t_pg_stat_user_tables = 11050 + t_pg_stat_xact_user_tables = 11054 + t_pg_statio_all_tables = 11057 + t_pg_statio_sys_tables = 11061 + t_pg_statio_user_tables = 11064 + t_pg_stat_all_indexes = 11067 + t_pg_stat_sys_indexes = 11071 + t_pg_stat_user_indexes = 11074 + t_pg_statio_all_indexes = 11077 + t_pg_statio_sys_indexes = 11081 + t_pg_statio_user_indexes = 11084 + t_pg_statio_all_sequences = 11087 + t_pg_statio_sys_sequences = 11090 + t_pg_statio_user_sequences = 11093 + t_pg_stat_activity = 11096 + t_pg_stat_replication = 11099 + t_pg_stat_database = 11102 + t_pg_stat_database_conflicts = 11105 + t_pg_stat_user_functions = 11108 + t_pg_stat_xact_user_functions = 11112 + t_pg_stat_bgwriter = 11116 + t_pg_user_mappings = 11119 + t_cardinal_number = 11669 + t_character_data = 11671 + t_sql_identifier = 11672 + t_information_schema_catalog_name = 11674 + t_time_stamp = 11676 + t_yes_or_no = 11677 + t_applicable_roles = 11680 + t_administrable_role_authorizations = 11684 + t_attributes = 11687 + t_character_sets = 11691 + t_check_constraint_routine_usage = 11695 + t_check_constraints = 11699 + t_collations = 11703 + t_collation_character_set_applicability = 11706 + t_column_domain_usage = 11709 + t_column_privileges = 11713 + t_column_udt_usage = 11717 + t_columns = 11721 + t_constraint_column_usage = 11725 + t_constraint_table_usage = 11729 + t_domain_constraints = 11733 + t_domain_udt_usage = 11737 + t_domains = 11740 + t_enabled_roles = 11744 + t_key_column_usage = 11747 + t_parameters = 11751 + t_referential_constraints = 11755 + t_role_column_grants = 11759 + t_routine_privileges = 11762 + t_role_routine_grants = 11766 + t_routines = 11769 + t_schemata = 11773 + t_sequences = 11776 + t_sql_features = 11780 + t_pg_toast_11779 = 11782 + t_sql_implementation_info = 11785 + t_pg_toast_11784 = 11787 + t_sql_languages = 11790 + t_pg_toast_11789 = 11792 + t_sql_packages = 11795 + t_pg_toast_11794 = 11797 + t_sql_parts = 11800 + t_pg_toast_11799 = 11802 + t_sql_sizing = 11805 + t_pg_toast_11804 = 11807 + t_sql_sizing_profiles = 11810 + t_pg_toast_11809 = 11812 + t_table_constraints = 11815 + t_table_privileges = 11819 + t_role_table_grants = 11823 + t_tables = 11826 + t_triggered_update_columns = 11830 + t_triggers = 11834 + t_usage_privileges = 11838 + t_role_usage_grants = 11842 + t_view_column_usage = 11845 + t_view_routine_usage = 11849 + t_view_table_usage = 11853 + t_views = 11857 + t_data_type_privileges = 11861 + t_element_types = 11865 + t__pg_foreign_data_wrappers = 11869 + t_foreign_data_wrapper_options = 11872 + t_foreign_data_wrappers = 11875 + t__pg_foreign_servers = 11878 + t_foreign_server_options = 11882 + t_foreign_servers = 11885 + t__pg_foreign_tables = 11888 + t_foreign_table_options = 11892 + t_foreign_tables = 11895 + t__pg_user_mappings = 11898 + t_user_mapping_options = 11901 + t_user_mappings = 11905 + t_t = 16806 + t__t = 16805 + t_temp = 16810 + t__temp = 16809 +) diff --git a/vendor/github.com/bmizerany/pq/url.go b/vendor/github.com/bmizerany/pq/url.go new file mode 100644 index 00000000..4e32cea8 --- /dev/null +++ b/vendor/github.com/bmizerany/pq/url.go @@ -0,0 +1,68 @@ +package pq + +import ( + "fmt" + nurl "net/url" + "sort" + "strings" +) + +// ParseURL converts url to a connection string for driver.Open. +// Example: +// +// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full" +// +// converts to: +// +// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full" +// +// A minimal example: +// +// "postgres://" +// +// This will be blank, causing driver.Open to use all of the defaults +func ParseURL(url string) (string, error) { + u, err := nurl.Parse(url) + if err != nil { + return "", err + } + + if u.Scheme != "postgres" { + return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) + } + + var kvs []string + accrue := func(k, v string) { + if v != "" { + kvs = append(kvs, k+"="+v) + } + } + + if u.User != nil { + v := u.User.Username() + accrue("user", v) + + v, _ = u.User.Password() + accrue("password", v) + } + + i := strings.Index(u.Host, ":") + if i < 0 { + accrue("host", u.Host) + } else { + accrue("host", u.Host[:i]) + accrue("port", u.Host[i+1:]) + } + + if u.Path != "" { + accrue("dbname", u.Path[1:]) + } + + q := u.Query() + for k, _ := range q { + accrue(k, q.Get(k)) + } + + sort.Strings(kvs) // Makes testing easier (not a performance concern) + return strings.Join(kvs, " "), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b6a35bf2..4e27b6f2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,7 @@ +# gitee.com/chunanyong/zorm v1.6.6 +## explicit; go 1.13 +gitee.com/chunanyong/zorm +gitee.com/chunanyong/zorm/decimal # github.com/aliyun/aliyun-oss-go-sdk v2.2.6+incompatible ## explicit github.com/aliyun/aliyun-oss-go-sdk/oss @@ -21,6 +25,9 @@ github.com/baidubce/bce-sdk-go/util/log github.com/basgys/goxml2json # github.com/bitly/go-simplejson v0.5.0 ## explicit +# github.com/bmizerany/pq v0.0.0-20131128184720-da2b95e392c1 +## explicit +github.com/bmizerany/pq # github.com/bytedance/sonic v1.8.1 ## explicit; go 1.15 github.com/bytedance/sonic