├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── api.go ├── condition.go ├── data_source.go ├── db.go ├── db_test.go ├── example.go ├── example └── internal │ ├── connect │ └── main.go │ ├── db │ └── db.go │ ├── delete │ └── main.go │ ├── insert │ └── main.go │ ├── insert_model │ └── main.go │ ├── migrate │ ├── main.go │ └── migrate │ │ └── 20201004160444_user.go │ ├── model │ ├── user.go │ ├── user_address.go │ └── user_with_address.go │ ├── query │ └── main.go │ ├── relation │ └── main.go │ └── update │ └── main.go ├── generics ├── go.mod └── query.go ├── go.mod ├── go.sum ├── id.go ├── incr_decr.go ├── interface.go ├── logger.go ├── media └── debug.png ├── migrate.go ├── migrate_test.go ├── model_test.go ├── op.go ├── package.json ├── query_builder.go ├── query_builder_test.go ├── queue.go ├── queue_dead_letter.go ├── queue_message.go ├── queue_test.go ├── render.go ├── result.go ├── scan_func.go ├── sql_checker.go ├── sql_checker_test.go ├── tag.go ├── transaction.go ├── type_create_update_time.go ├── type_soft_delete.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | generics/go.work 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: / 3 | sidebarBasedOnContent: true 4 | --- 5 | 6 | # goclub/sql 7 | [![Go Reference](https://pkg.go.dev/badge/github.com/goclub/sql.svg)](https://pkg.go.dev/github.com/goclub/sql) 8 | 9 | 10 | > goclub/sql 让你了解每一个函数执行的sql是什么,保证SQL的性能最大化的同时超越ORM的便捷。 11 | 12 | ## 指南 13 | 14 | 在 Go 中与 sql 交互一般会使用 [sqlx](https://github.com/jmoiron/sqlx) 或 [gorm](http://gorm.io/) [xorm](https://xorm.io/zh/) 15 | 16 | `sqlx` 偏底层,是对 `database/sql` 的封装,主要提供了基于结构体标签 `db:"name""`将查询结果解析为结构体的功能。而GORM XORM 功能则更丰富。 17 | 18 | 直接使用 `sqlx` 频繁的手写 sql 非常繁琐且容易出错。(database/sql 的接口设计的不是很友好)。 19 | 20 | GORM XORM 存在 ORM 都有的特点,使用者容易使用 ORM 运行一些性能不高的 SQL。虽然合理使用也可以写出高效SQL,但使用者在使用 ORM 的时候容易忽略最终运行的SQL是什么。 21 | 22 | [goclub/sql](https://github.com/goclub/sql) 提供介于手写 sql 和 ORM 之间的使用体验。 23 | 24 | 25 | ## Open 26 | 27 | > 连接数据库 28 | 29 | goclub/sql 与 database/sql 连接方式相同,只是多返回了 dbClose 函数。 `dbClose` 等同于 `db.Close` 30 | 31 | [Open](./example/internal/connect/main.go?embed) 32 | 33 | 34 | ## ExecMigrate 35 | 36 | > 通过迁移代码创建表结构 37 | 38 | [创建用户迁移文件](./example/internal/migrate/migrate/20201004160444_user.go?embed) 39 | 40 | [ExecMigrate](./example/internal/migrate/main.go?embed) 41 | 42 | ## 定义Model 43 | 44 | 通过表单创建 Model: [goclub.run](https://goclub.run/?k=model) 45 | 46 | ## Insert 47 | 48 | > 使用 Insert 插入数据 49 | 50 | [Insert](./example/internal/insert/main.go?embed) 51 | 52 | ## InsertModel 53 | 54 | > 基于 Model 插入数据 55 | 56 | 大部分场景下使用 `db.Insert` 插入数据有点繁琐。基于 `sq.Model` 使用 `db.InsertModel`操作数据会方便很多。 57 | 58 | [InsertModel](./example/internal/insert_model/main.go?embed) 59 | 60 | 61 | ## Update 62 | 63 | > 使用 Update 更新数据 64 | 65 | [Update](./example/internal/update/main.go?embed) 66 | 67 | > goclub/sql 故意没有提供 UpdateModel 方法, 因为使用 UpdateModel 性能并不好,会修改一下原本不需要修改的数据. 使用 `db.Update(ctx, sq.QB{...})` 可以"精准"的更新数据 68 | 69 | ## Query 70 | 71 | > 使用 Query 查询单条数据 72 | > 使用 QuerySlice 查询多条数据 73 | 74 | [Query](./example/internal/query/main.go?embed) 75 | 76 | > goclub/sql 特意没有提供 QueryModel 方法, 使用 `db.Query(ctx, &user, sq.QB{ Where: sq.And(col.ID, sq.Equal(userID)) })` 可以查询 Model 77 | 78 | ## SoftDelete HardDelete 79 | 80 | > 使用SoftDelete 或者 HardDelete 删除数据 81 | 82 | [delete](./example/internal/delete/main.go?embed) 83 | 84 | ## Relation 85 | 86 | [relation](./example/internal/relation/main.go?embed) 87 | 88 | ## Debug 89 | 90 | ```go 91 | sq.QB{ 92 | Debug: true 93 | } 94 | ``` 95 | 96 | 打开Debug可以查看 97 | 98 | 1. 运行的SQL 99 | 1. explain 100 | 1. 执行时间 101 | 1. last_query_cost 102 | 103 | ![](./media/debug.png) 104 | 105 | 你也可以单独打开某一项或几项 106 | 107 | ```go 108 | sq.QB{ 109 | PrintSQL: true, 110 | } 111 | 112 | sq.QB{ 113 | Explain: true, 114 | } 115 | 116 | sq.QB{ 117 | RunTime: true, 118 | } 119 | 120 | sq.QB{ 121 | LastQueryCost: true, 122 | } 123 | ``` 124 | 125 | ## Review 126 | 127 | Review 的作用是用于审查 sql 或增加代码可读性 128 | 129 | ### {#IN#} 130 | 131 | > 语法: {#IN#} 132 | 133 | 默认会直接与执行SQL进行比对, 执行SQL与Review不一致则会在运行时 print 错误. 134 | 135 | 有时候执行的SQL不是固定的字符串例如 136 | 137 | where in 时会根据查询条件不同导致有多种情况 138 | ``` 139 | select * from user where id in (?) 140 | select * from user where id in (?,?) 141 | select * from user where id in (?,?,?) 142 | ... 143 | 144 | ``` 145 | 虽然可以使用 Reviews 配置多个review 146 | ```go 147 | sq.QB{ 148 | Review: []string{ 149 | "select * from user where id in (?)", 150 | "select * from user where id in (?,?)", 151 | "select * from user where id in (?,?,?), 152 | }, 153 | } 154 | ``` 155 | 156 | 但这样无法覆盖全部的情况. 157 | 158 | 可以使用 `{#IN#}` 模糊匹配 159 | ```go 160 | sq.QB{ 161 | Review: "select * from user where id in {#IN#}" 162 | } 163 | ``` 164 | 165 | ### 零次一次 166 | 167 | 语法 168 | 169 | {{#任意字符#}} 170 | 171 | 如果你使用了 `sq.IF` 你可能需要用到 Reviews 172 | 173 | ```go 174 | sq.QB{ 175 | From: &User{}, 176 | Select: []sq.Column{"id"}, 177 | Where: sq.And("name", sq.IF(searchName != "", sq.Equal(searchName))), 178 | Reviews: []string{ 179 | "SELECT `id` FROM `user` WHERE `name` = ? AND `deleted_at` IS NULL", 180 | "SELECT `id` FROM `user` WHERE `deleted_at` IS NULL", 181 | }, 182 | } 183 | ``` 184 | 185 | 186 | 你可以使用 {{# and name = ?#}} 代替多个 review 187 | 建议将空格前置:使用 {{# and name = ?#}}, 而不是 {{#and name = ? #}}` 188 | 189 |
190 | 
191 | sq.QB{
192 |     From: &User{},
193 |     Select: []sq.Column{"id"},
194 |     Where: sq.And("name", sq.IF(searchName != "", sq.Equal(searchName))),
195 |     Review: "SELECT `id` FROM `user` WHERE{{# and name = ?#}} AND `deleted_at` IS NULL",
196 |     },
197 | }
198 | 
199 | 
200 | 201 | ### {#VALUES#} 202 | 203 | > 语法: `{#VALUES#}` 204 | 205 | 一些 Insert 语句会出现 `(?,?)` `(?,?),(?,?)` 的情况 206 | 207 | ``` 208 | INSERT INTO `user` (`name`,`age`) VALUES (?,?),(?,?) 209 | INSERT INTO `user` (`name`,`age`) VALUES (?,?) 210 | ``` 211 | 212 | 可以使用 `{#VALUES#}` 模糊匹配 213 | 214 | ```go 215 | sq.QB{ 216 | Review: "INSERT INTO `user` (`name`,`age`) VALUES {#VALUES#}" 217 | } 218 | ``` 219 | 220 | ## 致谢 221 | 222 | > 感谢 [jetbrains](https://jb.gg/OpenSource) 提供 Goland 开源授权 223 | 224 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: 2type/gitbook 2 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | type API interface { 9 | // Ping 检查连通性 10 | Ping(ctx context.Context) error 11 | // Close 关闭数据库连接 12 | Close() error 13 | 14 | // Insert 插入数据 15 | Insert(ctx context.Context, qb QB) (result Result, err error) 16 | // InsertAffected 插入数据(返回影响行数) 17 | InsertAffected(ctx context.Context, qb QB) (affected int64, err error) 18 | // InsertModel 基于 Model 创建数据, 根据 Model 字段自动填充 qb.Insert 19 | InsertModel(ctx context.Context, ptr Model, qb QB) (err error) 20 | // InsertModelAffected 基于 Model 创建数据 (返回影响行数) 21 | InsertModelAffected(ctx context.Context, ptr Model, qb QB) (affected int64, err error) 22 | // QueryRow 查询单行多列 类似 sql.Row{}.Scan() 23 | QueryRow(ctx context.Context, qb QB, desc []interface{}) (has bool, err error) 24 | // Query 查询单行多列并转换为结构体 25 | Query(ctx context.Context, ptr Tabler, qb QB) (has bool, err error) 26 | // QuerySlice 查询多行并转换为结构体 27 | QuerySlice(ctx context.Context, slicePtr interface{}, qb QB) (err error) 28 | // QuerySliceScaner 查询多行多列(自定义扫描) 29 | QuerySliceScaner(ctx context.Context, qb QB, scaner Scaner) (err error) 30 | 31 | QueryRelation(ctx context.Context, ptr Relation, qb QB) (has bool, err error) 32 | // QueryRelationSlice 查询多条数据并转换为 Relation slice 33 | QueryRelationSlice(ctx context.Context, relationSlicePtr interface{}, qb QB) (err error) 34 | // Count SELECT count(*) 35 | Count(ctx context.Context, from Tabler, qb QB) (count uint64, err error) 36 | // Has 查询数据是否存在(单条数据是否存在不建议使用 count 而是使用 Exist) 37 | Has(ctx context.Context, from Tabler, qb QB) (has bool, err error) 38 | SumInt64(ctx context.Context, from Tabler, column Column, qb QB) (value sql.NullInt64, err error) 39 | SumFloat64(ctx context.Context, from Tabler, column Column, qb QB) (value sql.NullFloat64, err error) 40 | 41 | // Update 更新 42 | Update(ctx context.Context, from Tabler, qb QB) (err error) 43 | // UpdateAffected 更新(返回影响行数) 44 | UpdateAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) 45 | // ClearTestData 删除测试数据库的数据,只能运行在 test_ 为前缀的数据库中 46 | ClearTestData(ctx context.Context, form Tabler, qb QB) (err error) 47 | // // 基于 Model 删除测试数据库的数据,只能运行在 test_ 为前缀的数据库中 48 | // ClearTestModel(ctx context.Context, model Model, qb QB) (result Result, err error) 49 | 50 | // HardDelete 硬删除(不可恢复) 51 | HardDelete(ctx context.Context, form Tabler, qb QB) (err error) 52 | // HardDeleteAffected 硬删除(不可恢复)(返回影响行数) 53 | HardDeleteAffected(ctx context.Context, form Tabler, qb QB) (affected int64, err error) 54 | // SoftDelete 软删除(可恢复) 55 | SoftDelete(ctx context.Context, form Tabler, qb QB) (err error) 56 | // SoftDeleteAffected 软删除(可恢复)(返回影响行数) 57 | SoftDeleteAffected(ctx context.Context, form Tabler, qb QB) (affected int64, err error) 58 | 59 | // ExecQB 执行QB 60 | ExecQB(ctx context.Context, qb QB, statement Statement) (result Result, err error) 61 | // ExecQBAffected 执行QB(返回影响行数) 62 | ExecQBAffected(ctx context.Context, qb QB, statement Statement) (affected int64, err error) 63 | // Exec 执行 64 | Exec(ctx context.Context, query string, values []interface{}) (result Result, err error) 65 | 66 | // Begin 开启事务 67 | Begin(ctx context.Context, level sql.IsolationLevel, handle func(tx *T) TxResult) (rollbackNoError bool, err error) 68 | BeginOpt(ctx context.Context, opt sql.TxOptions, handle func(tx *T) TxResult) (rollbackNoError bool, err error) 69 | // LastQueryCost show status like "last_query_cost" 70 | LastQueryCost(ctx context.Context) (lastQueryCost float64, err error) 71 | // PrintLastQueryCost 打印 show status like "last_query_cost" 的结果 72 | PrintLastQueryCost(ctx context.Context) 73 | // PublishMessage 发布消息 74 | PublishMessage(ctx context.Context, queueName string, publish Publish) (message Message, err error) 75 | // ConsumeMessage 消费消息 76 | ConsumeMessage(ctx context.Context, consume Consume) error 77 | } 78 | 79 | func verifyDoc() { 80 | db := &Database{} 81 | func(API) { 82 | 83 | }(db) 84 | tx := struct { 85 | T 86 | onlyDB 87 | }{} 88 | func(API) { 89 | 90 | }(&tx) 91 | } 92 | 93 | type onlyDB struct{} 94 | 95 | func (onlyDB) Ping(ctx context.Context) error { 96 | return nil 97 | } 98 | func (onlyDB) Close() error { 99 | return nil 100 | } 101 | func (onlyDB) ClearTestData(ctx context.Context, form Tabler, qb QB) (err error) { 102 | return 103 | } 104 | func (onlyDB) Begin(ctx context.Context, level sql.IsolationLevel, handle func(tx *T) TxResult) (rollbackNoError bool, err error) { 105 | return 106 | } 107 | func (onlyDB) BeginOpt(ctx context.Context, opt sql.TxOptions, handle func(tx *T) TxResult) (rollbackNoError bool, err error) { 108 | return 109 | } 110 | func (onlyDB) ConsumeMessage(ctx context.Context, consume Consume) (err error) { 111 | return 112 | } 113 | -------------------------------------------------------------------------------- /condition.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | const sqlPlaceholder = "?" 4 | 5 | type Condition struct { 6 | Column Column 7 | OP OP 8 | } 9 | 10 | func ConditionRaw(query string, values []interface{}) Condition { 11 | return Condition{ 12 | OP: OP{ 13 | Query: query, 14 | Values: values, 15 | }, 16 | } 17 | } 18 | func And(column Column, operator OP) conditions { 19 | return conditions{}.And(column, operator) 20 | } 21 | func AndRaw(query string, values ...interface{}) conditions { 22 | return And("", OP{ 23 | Query: query, 24 | Values: values, 25 | }) 26 | } 27 | func OrGroup(conditions ...Condition) conditions { 28 | op := OP{OrGroup: conditions} 29 | item := Condition{OP: op} 30 | return []Condition{item} 31 | } 32 | func ToConditions(c []Condition) conditions { 33 | return conditions(c) 34 | } 35 | 36 | type conditions []Condition 37 | 38 | func (w conditions) And(column Column, operator OP) conditions { 39 | w = append(w, Condition{ 40 | Column: column, 41 | OP: operator, 42 | }) 43 | return w 44 | } 45 | func (w conditions) AndRaw(query string, values ...interface{}) conditions { 46 | w = append(w, Condition{ 47 | Column: "", 48 | OP: OP{ 49 | Query: query, 50 | Values: values, 51 | }, 52 | }) 53 | return w 54 | } 55 | 56 | // func (w conditions) OrGroup(conditions []Condition) conditions { 57 | // op := OP{OrGroup: conditions} 58 | // item := Condition{OP:op} 59 | // w = append(w, item) 60 | // return w 61 | // } 62 | func ConditionsSQL(w [][]Condition) (raw Raw) { 63 | var orList stringQueue 64 | for _, whereAndList := range w { 65 | andsQV := ToConditions(whereAndList).coreSQL("AND") 66 | if len(andsQV.Query) != 0 { 67 | orList.Push(andsQV.Query) 68 | raw.Values = append(raw.Values, andsQV.Values...) 69 | } 70 | } 71 | raw.Query = orList.Join(") OR (") 72 | if len(orList.Value) > 1 { 73 | raw.Query = "(" + raw.Query + ")" 74 | } 75 | return 76 | } 77 | func (w conditions) coreSQL(split string) Raw { 78 | var andList stringQueue 79 | var values []interface{} 80 | for _, c := range w { 81 | if c.OP.Ignore { 82 | continue 83 | } 84 | sql := c.OP.sql(c.Column, &values) 85 | if len(sql) != 0 { 86 | andList.Push(sql) 87 | } 88 | } 89 | query := andList.Join(" " + split + " ") 90 | return Raw{query, values} 91 | } 92 | -------------------------------------------------------------------------------- /data_source.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | type MysqlDataSource struct { 9 | User string `yaml:"user"` 10 | Password string `yaml:"password"` 11 | Host string `yaml:"host"` 12 | Port string `yaml:"port"` 13 | DB string `yaml:"db"` 14 | // DefaultQuery 15 | // map[string]string{ 16 | // "charset": "utf8", 17 | // "parseTime": "True", 18 | // "loc": "Local", 19 | // } 20 | Query map[string]string `yaml:"query"` 21 | } 22 | 23 | func (config MysqlDataSource) FormatDSN() (dataSourceName string) { 24 | configList := []string{ 25 | config.User, 26 | ":", 27 | config.Password, 28 | "@", 29 | "(", 30 | config.Host, 31 | ":", 32 | config.Port, 33 | ")", 34 | "/", 35 | config.DB, 36 | "?", 37 | } 38 | configList = append(configList) 39 | if config.Query == nil { 40 | config.Query = map[string]string{ 41 | "charset": "utf8mb4", 42 | "parseTime": "True", 43 | "loc": "Local", 44 | } 45 | } 46 | values := url.Values{} 47 | for key, value := range config.Query { 48 | values.Set(key, value) 49 | } 50 | dataSourceName = strings.Join(configList, "") + values.Encode() 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | xerr "github.com/goclub/error" 7 | "github.com/jmoiron/sqlx" 8 | "reflect" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Database struct { 14 | Core *sqlx.DB 15 | SQLChecker SQLChecker 16 | QueueTimeLocation *time.Location 17 | } 18 | 19 | func (db *Database) Ping(ctx context.Context) error { 20 | return db.Core.PingContext(ctx) 21 | } 22 | func (db *Database) getCore() (core StoragerCore) { 23 | return db.Core 24 | } 25 | func (db *Database) getSQLChecker() SQLChecker { 26 | return db.SQLChecker 27 | } 28 | func Open(driverName string, dataSourceName string) (db *Database, dbClose func() error, err error) { 29 | var coreDatabase *sqlx.DB 30 | coreDatabase, err = sqlx.Open(driverName, dataSourceName) 31 | db = &Database{ 32 | Core: coreDatabase, 33 | SQLChecker: &DefaultSQLChecker{}, 34 | QueueTimeLocation: time.Local, 35 | } 36 | if err != nil && coreDatabase != nil { 37 | dbClose = func() error { 38 | // 忽略 log sync 错误 39 | _ = Log.Sync() 40 | return coreDatabase.Close() 41 | } 42 | } else { 43 | dbClose = func() error { return nil } 44 | } 45 | if err != nil { 46 | return 47 | } 48 | return 49 | } 50 | func (db *Database) Close() error { 51 | if db.Core != nil { 52 | return db.Core.Close() 53 | } 54 | Log.Warn("Database is nil,maybe you forget sq.Open()") 55 | return nil 56 | } 57 | 58 | var createTimeField = []string{"CreatedAt", "GMTCreate", "CreateTime"} 59 | var updateTimeField = []string{"UpdatedAt", "GMTModified", "UpdateTime"} 60 | var createAndUpdateTimeField = append(createTimeField, updateTimeField...) 61 | 62 | func (db *Database) Insert(ctx context.Context, qb QB) (result Result, err error) { 63 | return coreInsert(ctx, db, qb) 64 | } 65 | func (db *Database) InsertAffected(ctx context.Context, qb QB) (affected int64, err error) { 66 | return RowsAffected(db.Insert(ctx, qb)) 67 | } 68 | func (tx *T) Insert(ctx context.Context, qb QB) (result Result, err error) { 69 | return coreInsert(ctx, tx, qb) 70 | } 71 | func (tx *T) InsertAffected(ctx context.Context, qb QB) (affected int64, err error) { 72 | return RowsAffected(tx.Insert(ctx, qb)) 73 | } 74 | func coreInsert(ctx context.Context, storager Storager, qb QB) (result Result, err error) { 75 | defer func() { 76 | if err != nil { 77 | err = xerr.WithStack(err) 78 | } 79 | }() 80 | qb.SQLChecker = storager.getSQLChecker() 81 | qb.execDebugBefore(ctx, storager, StatementInsert) 82 | defer qb.execDebugAfter(ctx, storager, StatementInsert) 83 | return coreExecQB(ctx, storager, qb, StatementInsert) 84 | } 85 | 86 | func (db *Database) InsertModel(ctx context.Context, ptr Model, qb QB) (err error) { 87 | if _, err = coreInsertModel(ctx, db, ptr, qb); err != nil { 88 | return 89 | } 90 | return 91 | } 92 | func (db *Database) InsertModelAffected(ctx context.Context, ptr Model, qb QB) (affected int64, err error) { 93 | return RowsAffected(coreInsertModel(ctx, db, ptr, qb)) 94 | } 95 | func (tx *T) InsertModel(ctx context.Context, ptr Model, qb QB) (err error) { 96 | if _, err = coreInsertModel(ctx, tx, ptr, qb); err != nil { 97 | return 98 | } 99 | return 100 | } 101 | func (tx *T) InsertModelAffected(ctx context.Context, ptr Model, qb QB) (affected int64, err error) { 102 | return RowsAffected(coreInsertModel(ctx, tx, ptr, qb)) 103 | } 104 | 105 | func coreInsertModel(ctx context.Context, storager Storager, ptr Model, qb QB) (result Result, err error) { 106 | defer func() { 107 | if err != nil { 108 | err = xerr.WithStack(err) 109 | } 110 | }() 111 | err = ptr.BeforeInsert() 112 | if err != nil { 113 | return 114 | } 115 | if qb.From != nil { 116 | Log.Warn("InsertModel(ctx, qb, model) qb.From need be nil") 117 | } 118 | qb.From = ptr 119 | qb.SQLChecker = storager.getSQLChecker() 120 | rValue := reflect.ValueOf(ptr) 121 | rType := rValue.Type() 122 | if rType.Kind() != reflect.Ptr { 123 | return result, xerr.New("InsertModel(ctx, ptr) " + rType.String() + " must be ptr") 124 | } 125 | elemValue := rValue.Elem() 126 | elemType := rType.Elem() 127 | if len(qb.Insert) == 0 && len(qb.InsertMultiple.Column) == 0 { 128 | insertEachField(elemValue, elemType, func(column string, fieldType reflect.StructField, fieldValue reflect.Value) { 129 | qb.Insert = append(qb.Insert, Insert{Column: Column(column), Value: fieldValue.Interface()}) 130 | }) 131 | } 132 | raw := qb.SQLInsert() 133 | query, values := raw.Query, raw.Values 134 | qb.execDebugBefore(ctx, storager, StatementInsert) 135 | defer qb.execDebugAfter(ctx, storager, StatementInsert) 136 | result.core, err = storager.getCore().ExecContext(ctx, query, values...) 137 | if err != nil { 138 | return 139 | } 140 | err = ptr.AfterInsert(result) 141 | if err != nil { 142 | return 143 | } 144 | return 145 | } 146 | func insertEachField(elemValue reflect.Value, elemType reflect.Type, handle func(column string, fieldType reflect.StructField, fieldValue reflect.Value)) { 147 | for i := 0; i < elemType.NumField(); i++ { 148 | fieldType := elemType.Field(i) 149 | fieldValue := elemValue.Field(i) 150 | // `db:"name"` 151 | column, hasDBTag := fieldType.Tag.Lookup("db") 152 | if fieldType.Anonymous == true { 153 | insertEachField(fieldValue, fieldValue.Type(), handle) 154 | continue 155 | } 156 | if !hasDBTag { 157 | continue 158 | } 159 | if column == "" { 160 | continue 161 | } 162 | // `sq:"ignoreInsert"` 163 | shouldIgnoreInsert := Tag{fieldType.Tag.Get("sq")}.IsIgnoreInsert() 164 | if shouldIgnoreInsert { 165 | continue 166 | } 167 | // created updated time.Time 168 | for _, timeField := range createAndUpdateTimeField { 169 | if fieldType.Name == timeField { 170 | setTimeNow(fieldValue, fieldType) 171 | } 172 | } 173 | handle(column, fieldType, fieldValue) 174 | } 175 | } 176 | func (db *Database) QueryRow(ctx context.Context, qb QB, desc []interface{}) (has bool, err error) { 177 | err = qb.mustInTransaction() 178 | if err != nil { 179 | return 180 | } 181 | return coreQueryRowScan(ctx, db, qb, desc) 182 | } 183 | func (tx *T) QueryRow(ctx context.Context, qb QB, desc []interface{}) (has bool, err error) { 184 | return coreQueryRowScan(ctx, tx, qb, desc) 185 | } 186 | func coreQueryRowScan(ctx context.Context, storager Storager, qb QB, desc []interface{}) (has bool, err error) { 187 | defer func() { 188 | if err != nil { 189 | err = xerr.WithStack(err) 190 | } 191 | }() 192 | qb.SQLChecker = storager.getSQLChecker() 193 | qb.Limit = 1 194 | raw := qb.SQLSelect() 195 | query, values := raw.Query, raw.Values 196 | qb.execDebugBefore(ctx, storager, StatementSelect) 197 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 198 | row := storager.getCore().QueryRowxContext(ctx, query, values...) 199 | scanErr := row.Scan(desc...) 200 | has, err = CheckRowScanErr(scanErr) 201 | if err != nil { 202 | return 203 | } 204 | return 205 | } 206 | func (db *Database) QuerySliceScaner(ctx context.Context, qb QB, scan Scaner) (err error) { 207 | err = qb.mustInTransaction() 208 | if err != nil { 209 | return 210 | } 211 | return coreQuerySliceScaner(ctx, db, qb, scan) 212 | } 213 | func (tx *T) QuerySliceScaner(ctx context.Context, qb QB, scan Scaner) error { 214 | return coreQuerySliceScaner(ctx, tx, qb, scan) 215 | } 216 | func coreQuerySliceScaner(ctx context.Context, storager Storager, qb QB, scan Scaner) (err error) { 217 | defer func() { 218 | if err != nil { 219 | err = xerr.WithStack(err) 220 | } 221 | }() 222 | qb.SQLChecker = storager.getSQLChecker() 223 | raw := qb.SQLSelect() 224 | query, values := raw.Query, raw.Values 225 | qb.execDebugBefore(ctx, storager, StatementSelect) 226 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 227 | rows, err := storager.getCore().QueryxContext(ctx, query, values...) 228 | if err != nil { 229 | return err 230 | } 231 | defer func() { 232 | err = rows.Close() 233 | if err != nil { 234 | return 235 | } 236 | }() 237 | for rows.Next() { 238 | err := scan(rows) 239 | if err != nil { 240 | return err 241 | } 242 | } 243 | if rowsErr := rows.Err(); rowsErr != nil { 244 | return rowsErr 245 | } 246 | return nil 247 | } 248 | func (db *Database) Query(ctx context.Context, ptr Tabler, qb QB) (has bool, err error) { 249 | err = qb.mustInTransaction() 250 | if err != nil { 251 | return 252 | } 253 | return coreQuery(ctx, db, ptr, qb) 254 | } 255 | func (tx *T) Query(ctx context.Context, ptr Tabler, qb QB) (has bool, err error) { 256 | return coreQuery(ctx, tx, ptr, qb) 257 | } 258 | 259 | func coreQuery(ctx context.Context, storager Storager, ptr Tabler, qb QB) (has bool, err error) { 260 | defer func() { 261 | if err != nil { 262 | err = xerr.WithStack(err) 263 | } 264 | }() 265 | qb.SQLChecker = storager.getSQLChecker() 266 | qb.Limit = 1 267 | if qb.From == nil { 268 | qb.From = ptr 269 | } 270 | raw := qb.SQLSelect() 271 | query, values := raw.Query, raw.Values 272 | row := storager.getCore().QueryRowxContext(ctx, query, values...) 273 | qb.execDebugBefore(ctx, storager, StatementSelect) 274 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 275 | scanErr := row.StructScan(ptr) 276 | has, err = CheckRowScanErr(scanErr) 277 | if err != nil { 278 | return 279 | } 280 | return 281 | } 282 | 283 | func (db *Database) QuerySlice(ctx context.Context, slicePtr interface{}, qb QB) (err error) { 284 | err = qb.mustInTransaction() 285 | if err != nil { 286 | return 287 | } 288 | return coreQuerySlice(ctx, db, slicePtr, qb) 289 | } 290 | func (tx *T) QuerySlice(ctx context.Context, slicePtr interface{}, qb QB) (err error) { 291 | return coreQuerySlice(ctx, tx, slicePtr, qb) 292 | } 293 | func coreQuerySlice(ctx context.Context, storager Storager, slicePtr interface{}, qb QB) (err error) { 294 | defer func() { 295 | if err != nil { 296 | err = xerr.WithStack(err) 297 | } 298 | }() 299 | qb.SQLChecker = storager.getSQLChecker() 300 | ptrType := reflect.TypeOf(slicePtr) 301 | if ptrType.Kind() != reflect.Ptr { 302 | return xerr.New("goclub/sql: " + ptrType.String() + "not pointer") 303 | } 304 | if qb.From == nil && qb.FromRaw.TableName.Query == "" && qb.Raw.Query == "" { 305 | elemType := ptrType.Elem() 306 | reflectItemValue := reflect.MakeSlice(elemType, 1, 1).Index(0) 307 | if reflectItemValue.CanAddr() { 308 | reflectItemValue = reflectItemValue.Addr() 309 | } 310 | tablerInterface := reflectItemValue.Interface().(Tabler) 311 | qb.From = tablerInterface 312 | } 313 | if qb.From == nil && qb.FromRaw.TableName.Query == "" && qb.Raw.Query == "" { 314 | // 如果设置了 qb.Form 但没有设置 qb.Select 可能会导致 select * ,这种情况在代码已经在线上运行时但是表变动了时会很危险 315 | if len(qb.Select) == 0 && len(qb.SelectRaw) == 0 { 316 | err = xerr.New("goclub/sql: QuerySlice(ctx, slice, qb) if qb.Form/qb.FromRaw/qb.Raw not zero value, then qb.Select or qb.SelectRaw can not be nil, or you can set qb.Form/qb.FromRaw/ be nil") 317 | return 318 | } 319 | } 320 | raw := qb.SQLSelect() 321 | query, values := raw.Query, raw.Values 322 | qb.execDebugBefore(ctx, storager, StatementSelect) 323 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 324 | return storager.getCore().SelectContext(ctx, slicePtr, query, values...) 325 | } 326 | func (db *Database) Count(ctx context.Context, from Tabler, qb QB) (count uint64, err error) { 327 | err = qb.mustInTransaction() 328 | if err != nil { 329 | return 330 | } 331 | return coreCount(ctx, db, from, qb) 332 | } 333 | func (tx *T) Count(ctx context.Context, from Tabler, qb QB) (count uint64, err error) { 334 | return coreCount(ctx, tx, from, qb) 335 | } 336 | func coreCount(ctx context.Context, storager Storager, from Tabler, qb QB) (count uint64, err error) { 337 | defer func() { 338 | if err != nil { 339 | err = xerr.WithStack(err) 340 | } 341 | }() 342 | qb.From = from 343 | qb.SQLChecker = storager.getSQLChecker() 344 | if len(qb.SelectRaw) == 0 { 345 | qb.SelectRaw = []Raw{{"COUNT(*)", nil}} 346 | } 347 | qb.limitRaw = limitRaw{Valid: true, Limit: 0} 348 | qb.execDebugBefore(ctx, storager, StatementSelect) 349 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 350 | var has bool 351 | has, err = coreQueryRowScan(ctx, storager, qb, []interface{}{&count}) 352 | if err != nil { 353 | return 354 | } 355 | if has == false { 356 | raw := qb.SQLSelect() 357 | query := raw.Query 358 | return 0, xerr.New("goclub/sql: Count() " + query + "not found data") 359 | } 360 | return 361 | } 362 | 363 | // if you need query data exited SELECT "has" FROM user WHERE id = ? better than SELECT count(*) FROM user where id = ? 364 | func (db *Database) Has(ctx context.Context, from Tabler, qb QB) (has bool, err error) { 365 | err = qb.mustInTransaction() 366 | if err != nil { 367 | return 368 | } 369 | return coreHas(ctx, db, from, qb) 370 | } 371 | func (tx *T) Has(ctx context.Context, from Tabler, qb QB) (has bool, err error) { 372 | return coreHas(ctx, tx, from, qb) 373 | } 374 | func coreHas(ctx context.Context, storager Storager, from Tabler, qb QB) (has bool, err error) { 375 | defer func() { 376 | if err != nil { 377 | err = xerr.WithStack(err) 378 | } 379 | }() 380 | qb.From = from 381 | qb.SQLChecker = storager.getSQLChecker() 382 | qb.SelectRaw = []Raw{{`1`, nil}} 383 | qb.Limit = 1 384 | qb.execDebugBefore(ctx, storager, StatementSelect) 385 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 386 | var i int 387 | return coreQueryRowScan(ctx, storager, qb, []interface{}{&i}) 388 | } 389 | func (db *Database) SumInt64(ctx context.Context, from Tabler, column Column, qb QB) (value sql.NullInt64, err error) { 390 | err = coreSum(ctx, db, from, column, qb, &value) 391 | if err != nil { 392 | return 393 | } 394 | return value, err 395 | } 396 | func (tx *T) SumInt64(ctx context.Context, from Tabler, column Column, qb QB) (value sql.NullInt64, err error) { 397 | err = coreSum(ctx, tx, from, column, qb, &value) 398 | if err != nil { 399 | return 400 | } 401 | return value, err 402 | } 403 | func (db *Database) SumFloat64(ctx context.Context, from Tabler, column Column, qb QB) (value sql.NullFloat64, err error) { 404 | err = coreSum(ctx, db, from, column, qb, &value) 405 | if err != nil { 406 | return 407 | } 408 | return value, err 409 | } 410 | func (tx *T) SumFloat64(ctx context.Context, from Tabler, column Column, qb QB) (value sql.NullFloat64, err error) { 411 | err = coreSum(ctx, tx, from, column, qb, &value) 412 | if err != nil { 413 | return 414 | } 415 | return value, err 416 | } 417 | func coreSum(ctx context.Context, storager Storager, from Tabler, column Column, qb QB, valuePtr interface{}) (err error) { 418 | defer func() { 419 | if err != nil { 420 | err = xerr.WithStack(err) 421 | } 422 | }() 423 | qb.From = from 424 | qb.SQLChecker = storager.getSQLChecker() 425 | qb.SelectRaw = []Raw{{"SUM(" + column.wrapField() + ")", nil}} 426 | qb.limitRaw.Valid = true 427 | qb.limitRaw.Limit = 0 428 | qb.execDebugBefore(ctx, storager, StatementSelect) 429 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 430 | _, err = coreQueryRowScan(ctx, storager, qb, []interface{}{valuePtr}) 431 | if err != nil { 432 | return 433 | } 434 | return 435 | } 436 | 437 | func (db *Database) Update(ctx context.Context, from Tabler, qb QB) (err error) { 438 | if _, err = coreUpdate(ctx, db, from, qb); err != nil { 439 | return 440 | } 441 | return 442 | } 443 | func (db *Database) UpdateAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) { 444 | return RowsAffected(coreUpdate(ctx, db, from, qb)) 445 | } 446 | func (tx *T) Update(ctx context.Context, from Tabler, qb QB) (err error) { 447 | if _, err = coreUpdate(ctx, tx, from, qb); err != nil { 448 | return 449 | } 450 | return 451 | } 452 | func (tx *T) UpdateAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) { 453 | return RowsAffected(coreUpdate(ctx, tx, from, qb)) 454 | } 455 | func coreUpdate(ctx context.Context, storager Storager, from Tabler, qb QB) (result Result, err error) { 456 | defer func() { 457 | if err != nil { 458 | err = xerr.WithStack(err) 459 | } 460 | }() 461 | if qb.From == nil { 462 | qb.From = from 463 | } 464 | qb.SQLChecker = storager.getSQLChecker() 465 | raw := qb.SQLUpdate() 466 | query, values := raw.Query, raw.Values 467 | result.core, err = storager.getCore().ExecContext(ctx, query, values...) 468 | qb.execDebugBefore(ctx, storager, StatementUpdate) 469 | defer qb.execDebugAfter(ctx, storager, StatementUpdate) 470 | if err != nil { 471 | return result, err 472 | } 473 | return 474 | } 475 | 476 | func (db *Database) checkIsTestDatabase(ctx context.Context) (err error) { 477 | var databaseName string 478 | _, err = db.QueryRow(ctx, QB{Raw: Raw{"SELECT DATABASE()", nil}}, []interface{}{&databaseName}) 479 | if err != nil { 480 | return 481 | } 482 | if strings.HasPrefix(databaseName, "test_") == false { 483 | return xerr.New("ClearTestData only support delete test database") 484 | } 485 | return 486 | } 487 | func (db *Database) ClearTestData(ctx context.Context, from Tabler, qb QB) (err error) { 488 | defer func() { 489 | if err != nil { 490 | err = xerr.WithStack(err) 491 | } 492 | }() 493 | err = db.checkIsTestDatabase(ctx) 494 | if err != nil { 495 | return 496 | } 497 | return db.HardDelete(ctx, from, qb) 498 | } 499 | func (db *Database) HardDelete(ctx context.Context, from Tabler, qb QB) (err error) { 500 | if _, err = coreHardDelete(ctx, db, from, qb); err != nil { 501 | return 502 | } 503 | return 504 | } 505 | func (db *Database) HardDeleteAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) { 506 | return RowsAffected(coreHardDelete(ctx, db, from, qb)) 507 | } 508 | func (tx *T) HardDelete(ctx context.Context, from Tabler, qb QB) (err error) { 509 | if _, err = coreHardDelete(ctx, tx, from, qb); err != nil { 510 | return 511 | } 512 | return 513 | } 514 | func (tx *T) HardDeleteAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) { 515 | return RowsAffected(coreHardDelete(ctx, tx, from, qb)) 516 | } 517 | func coreHardDelete(ctx context.Context, storager Storager, from Tabler, qb QB) (result Result, err error) { 518 | defer func() { 519 | if err != nil { 520 | err = xerr.WithStack(err) 521 | } 522 | }() 523 | if qb.From == nil { 524 | qb.From = from 525 | } 526 | qb.SQLChecker = storager.getSQLChecker() 527 | raw := qb.SQLDelete() 528 | result.core, err = storager.getCore().ExecContext(ctx, raw.Query, raw.Values...) 529 | if err != nil { 530 | return 531 | } 532 | return 533 | } 534 | 535 | // func (db *Database) hardDeleteModel(ctx context.Context, ptr Model, qb QB) (result Result, err error){ 536 | // return coreHardDeleteModel(ctx,db, ptr, qb) 537 | // } 538 | // func (tx *T) HardDeleteModel(ctx context.Context, ptr Model, qb QB) (result Result, err error){ 539 | // return coreHardDeleteModel(ctx, tx, ptr, qb) 540 | // } 541 | // func coreHardDeleteModel(ctx context.Context, storager Storager, ptr Model, qb QB) (result Result, err error) { 542 | // defer func() { if err != nil { err = xerr.WithStack(err) } }() 543 | // rValue := reflect.ValueOf(ptr) 544 | // rType := rValue.Type() 545 | // if rType.Kind() != reflect.Ptr { 546 | // return result, xerr.New("UpdateModel(ctx, ptr) " + rType.String() + " must be ptr") 547 | // } 548 | // primaryKey, err := safeGetPrimaryKey(ptr); if err != nil { 549 | // return 550 | // } 551 | // qb.From = ptr 552 | // qb.Where = primaryKey 553 | // qb.Limit = 1 554 | // 555 | // qb.SQLChecker = storager.getSQLChecker() 556 | // raw := qb.SQLDelete() 557 | // qb.execDebugBefore(ctx, storager, StatementUpdate) 558 | // defer qb.execDebugAfter(ctx, storager, StatementUpdate) 559 | // return storager.getCore().ExecContext(ctx, raw.Query, raw.Values...) 560 | // } 561 | func (db *Database) SoftDelete(ctx context.Context, from Tabler, qb QB) (err error) { 562 | if _, err = coreSoftDelete(ctx, db, from, qb); err != nil { 563 | return 564 | } 565 | return 566 | } 567 | func (db *Database) SoftDeleteAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) { 568 | return RowsAffected(coreSoftDelete(ctx, db, from, qb)) 569 | } 570 | 571 | func (tx *T) SoftDelete(ctx context.Context, from Tabler, qb QB) (err error) { 572 | if _, err = coreSoftDelete(ctx, tx, from, qb); err != nil { 573 | return 574 | } 575 | return 576 | } 577 | func (tx *T) SoftDeleteAffected(ctx context.Context, from Tabler, qb QB) (affected int64, err error) { 578 | return RowsAffected(coreSoftDelete(ctx, tx, from, qb)) 579 | } 580 | func coreSoftDelete(ctx context.Context, storager Storager, from Tabler, qb QB) (result Result, err error) { 581 | defer func() { 582 | if err != nil { 583 | err = xerr.WithStack(err) 584 | } 585 | }() 586 | if qb.From == nil { 587 | qb.From = from 588 | } 589 | softDeleteWhere := qb.From.SoftDeleteWhere() 590 | if softDeleteWhere.Query == "" { 591 | err = xerr.New("goclub/sql: SoftDelete(ctx, qb) qb.Form.SoftDeleteWhere().Query can not be empty string") 592 | return 593 | } 594 | qb.SQLChecker = storager.getSQLChecker() 595 | softDeleteSet := qb.From.SoftDeleteSet() 596 | if softDeleteSet.Query == "" { 597 | err = xerr.New("goclub/sql: SoftDelete()" + qb.From.TableName() + "without soft delete set") 598 | return 599 | } 600 | qb.Set = []Update{ 601 | {Raw: softDeleteSet}, 602 | } 603 | raw := qb.SQLUpdate() 604 | qb.execDebugBefore(ctx, storager, StatementUpdate) 605 | defer qb.execDebugAfter(ctx, storager, StatementUpdate) 606 | result.core, err = storager.getCore().ExecContext(ctx, raw.Query, raw.Values...) 607 | if err != nil { 608 | return 609 | } 610 | return 611 | } 612 | func (db *Database) QueryRelation(ctx context.Context, ptr Relation, qb QB) (has bool, err error) { 613 | err = qb.mustInTransaction() 614 | if err != nil { 615 | return 616 | } 617 | return coreQueryRelation(ctx, db, ptr, qb) 618 | } 619 | func (tx *T) QueryRelation(ctx context.Context, ptr Relation, qb QB) (has bool, err error) { 620 | return coreQueryRelation(ctx, tx, ptr, qb) 621 | } 622 | func coreQueryRelation(ctx context.Context, storager Storager, ptr Relation, qb QB) (has bool, err error) { 623 | defer func() { 624 | if err != nil { 625 | err = xerr.WithStack(err) 626 | } 627 | }() 628 | qb.SQLChecker = storager.getSQLChecker() 629 | qb.Select = TagToColumns(ptr) 630 | table := table{ 631 | tableName: ptr.TableName(), 632 | softDeleteWhere: ptr.SoftDeleteWhere, 633 | // Relation 不需要 update 634 | softDeleteSet: func() Raw { return Raw{} }, 635 | } 636 | qb.From = table 637 | qb.Limit = 1 638 | qb.Join = ptr.RelationJoin() 639 | 640 | qb.SQLChecker = storager.getSQLChecker() 641 | qb.Limit = 1 642 | 643 | raw := qb.SQLSelect() 644 | query, values := raw.Query, raw.Values 645 | qb.execDebugBefore(ctx, storager, StatementSelect) 646 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 647 | row := storager.getCore().QueryRowxContext(ctx, query, values...) 648 | scanErr := row.StructScan(ptr) 649 | has, err = CheckRowScanErr(scanErr) 650 | if err != nil { 651 | return 652 | } 653 | return 654 | } 655 | func (db *Database) QueryRelationSlice(ctx context.Context, relationSlicePtr interface{}, qb QB) (err error) { 656 | err = qb.mustInTransaction() 657 | if err != nil { 658 | return 659 | } 660 | return coreQueryRelationSlice(ctx, db, relationSlicePtr, qb) 661 | } 662 | func (tx *T) QueryRelationSlice(ctx context.Context, relationSlicePtr interface{}, qb QB) (err error) { 663 | return coreQueryRelationSlice(ctx, tx, relationSlicePtr, qb) 664 | } 665 | func coreQueryRelationSlice(ctx context.Context, storager Storager, relationSlicePtr interface{}, qb QB) (err error) { 666 | defer func() { 667 | if err != nil { 668 | err = xerr.WithStack(err) 669 | } 670 | }() 671 | qb.SQLChecker = storager.getSQLChecker() 672 | ptrType := reflect.TypeOf(relationSlicePtr) 673 | if ptrType.Kind() != reflect.Ptr { 674 | return xerr.New("goclub/sql: " + ptrType.String() + "not pointer") 675 | } 676 | elemType := ptrType.Elem() 677 | reflectItemValue := reflect.MakeSlice(elemType, 1, 1).Index(0) 678 | if reflectItemValue.CanAddr() { 679 | reflectItemValue = reflectItemValue.Addr() 680 | } 681 | tablerInterface := reflectItemValue.Interface().(Relation) 682 | 683 | qb.Select = TagToColumns(tablerInterface) 684 | qb.From = table{ 685 | tableName: tablerInterface.TableName(), 686 | softDeleteWhere: tablerInterface.SoftDeleteWhere, 687 | // Relation 不需要 update 688 | softDeleteSet: func() Raw { return Raw{} }, 689 | } 690 | qb.Join = tablerInterface.RelationJoin() 691 | raw := qb.SQLSelect() 692 | query, values := raw.Query, raw.Values 693 | qb.execDebugBefore(ctx, storager, StatementSelect) 694 | defer qb.execDebugAfter(ctx, storager, StatementSelect) 695 | err = storager.getCore().SelectContext(ctx, relationSlicePtr, query, values...) 696 | if err != nil { 697 | return err 698 | } 699 | return 700 | } 701 | 702 | func (db *Database) Exec(ctx context.Context, query string, values []interface{}) (result Result, err error) { 703 | return coreExec(ctx, db, query, values) 704 | } 705 | func (tx *T) Exec(ctx context.Context, query string, values []interface{}) (result Result, err error) { 706 | return coreExec(ctx, tx, query, values) 707 | } 708 | func coreExec(ctx context.Context, storager Storager, query string, values []interface{}) (result Result, err error) { 709 | defer func() { 710 | if err != nil { 711 | err = xerr.WithStack(err) 712 | } 713 | }() 714 | result.core, err = storager.getCore().ExecContext(ctx, query, values...) 715 | if err != nil { 716 | return 717 | } 718 | return 719 | } 720 | func (db *Database) ExecQB(ctx context.Context, qb QB, statement Statement) (result Result, err error) { 721 | return coreExecQB(ctx, db, qb, statement) 722 | } 723 | func (db *Database) ExecQBAffected(ctx context.Context, qb QB, statement Statement) (affected int64, err error) { 724 | return RowsAffected(coreExecQB(ctx, db, qb, statement)) 725 | } 726 | func (tx *T) ExecQB(ctx context.Context, qb QB, statement Statement) (result Result, err error) { 727 | return coreExecQB(ctx, tx, qb, statement) 728 | } 729 | func (tx *T) ExecQBAffected(ctx context.Context, qb QB, statement Statement) (affected int64, err error) { 730 | return RowsAffected(coreExecQB(ctx, tx, qb, statement)) 731 | } 732 | func coreExecQB(ctx context.Context, storager Storager, qb QB, statement Statement) (result Result, err error) { 733 | defer func() { 734 | if err != nil { 735 | err = xerr.WithStack(err) 736 | } 737 | }() 738 | qb.SQLChecker = storager.getSQLChecker() 739 | raw := qb.SQL(statement) 740 | result.core, err = storager.getCore().ExecContext(ctx, raw.Query, raw.Values...) 741 | if err != nil { 742 | return 743 | } 744 | return 745 | } 746 | func (db *Database) LastQueryCost(ctx context.Context) (lastQueryCost float64, err error) { 747 | return coreLastQueryCost(ctx, db) 748 | } 749 | func (tx *T) LastQueryCost(ctx context.Context) (lastQueryCost float64, err error) { 750 | return coreLastQueryCost(ctx, tx) 751 | } 752 | func coreLastQueryCost(ctx context.Context, storager Storager) (lastQueryCost float64, err error) { 753 | defer func() { 754 | if err != nil { 755 | err = xerr.WithStack(err) 756 | } 757 | }() 758 | rows := storager.getCore().QueryRowxContext(ctx, `show status like 'last_query_cost'`) 759 | if err != nil { 760 | return 761 | } 762 | var name string 763 | err = rows.Scan(&name, &lastQueryCost) 764 | if err != nil { 765 | return 766 | } 767 | return 768 | } 769 | func (db *Database) PrintLastQueryCost(ctx context.Context) { 770 | corePrintLastQueryCost(ctx, db) 771 | } 772 | func (tx *T) PrintLastQueryCost(ctx context.Context) { 773 | corePrintLastQueryCost(ctx, tx) 774 | } 775 | func corePrintLastQueryCost(ctx context.Context, storager Storager) { 776 | cost, err := coreLastQueryCost(ctx, storager) 777 | if err != nil { 778 | Log.Debug("error", "error", err) 779 | } 780 | Log.Debug("last_query_cost", "cost", cost) 781 | } 782 | -------------------------------------------------------------------------------- /example.go: -------------------------------------------------------------------------------- 1 | package sq 2 | -------------------------------------------------------------------------------- /example/internal/connect/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | _ "github.com/go-sql-driver/mysql" 6 | sq "github.com/goclub/sql" 7 | "log" 8 | "time" 9 | ) 10 | 11 | var db *sq.Database 12 | 13 | func init() { 14 | var err error 15 | var dbClose func() error 16 | db, dbClose, err = sq.Open("mysql", sq.MysqlDataSource{ 17 | // 生产环境请使用环境变量或者配置中心配置数据库地址,不要硬编码在代码中 18 | User: "root", 19 | Password: "somepass", 20 | Host: "127.0.0.1", 21 | Port: "3306", 22 | DB: "example_goclub_sql", 23 | Query: map[string]string{ 24 | "charset": "utf8", 25 | "parseTime": "True", 26 | "loc": "Local", 27 | }, 28 | }.FormatDSN()) 29 | if err != nil { 30 | // 大部分创建数据库连接失败应该panic 31 | panic(err) 32 | } 33 | // 使用 init 方式连接数据库则无需 close ,程序退出再执行close 34 | _ = dbClose() 35 | } 36 | func main() { 37 | ctx := context.Background() 38 | // 设置ping超时1s则视为失败 39 | pingCtx, cancelFunc := context.WithTimeout(ctx, time.Second) 40 | defer cancelFunc() 41 | err := db.Ping(pingCtx) 42 | if err != nil { 43 | panic(err) 44 | } 45 | log.Print("连接成功") 46 | } 47 | -------------------------------------------------------------------------------- /example/internal/db/db.go: -------------------------------------------------------------------------------- 1 | package connectMysql 2 | 3 | import ( 4 | _ "github.com/go-sql-driver/mysql" 5 | sq "github.com/goclub/sql" 6 | ) 7 | 8 | var DB *sq.Database 9 | 10 | func init() { 11 | var err error 12 | var dbClose func() error 13 | DB, dbClose, err = sq.Open("mysql", sq.MysqlDataSource{ 14 | // 生产环境请使用环境变量或者配置中心配置数据库地址,不要硬编码在代码中 15 | User: "root", 16 | Password: "somepass", 17 | Host: "127.0.0.1", 18 | Port: "3306", 19 | DB: "example_goclub_sql", 20 | Query: map[string]string{ 21 | "charset": "utf8", 22 | "parseTime": "True", 23 | "loc": "Local", 24 | }, 25 | }.FormatDSN()) 26 | if err != nil { 27 | // 大部分创建数据库连接失败应该panic 28 | panic(err) 29 | } 30 | // 使用 init 方式连接数据库则无需 close ,依赖注入场景下才需要 close 31 | _ = dbClose() 32 | } 33 | -------------------------------------------------------------------------------- /example/internal/delete/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | connectMysql "github.com/goclub/sql/example/internal/db" 7 | m "github.com/goclub/sql/example/internal/model" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | err := example(ctx) 14 | if err != nil { 15 | log.Print(err) 16 | } 17 | } 18 | func example(ctx context.Context) (err error) { 19 | db := connectMysql.DB 20 | col := m.TableUser{}.Column() 21 | // 通过 InsertModel 准备数据 22 | insertUser := m.User{ 23 | Name: "delete1", 24 | Mobile: "13400001111", 25 | ChinaIDCardNo: "340828199912121111", 26 | } 27 | err = db.InsertModel(ctx, &insertUser, sq.QB{ 28 | UseInsertIgnoreInto: true, 29 | }) 30 | if err != nil { 31 | return 32 | } 33 | userID := insertUser.ID 34 | // 软删 35 | err = db.SoftDelete(ctx, sq.QB{ 36 | From: &m.TableUser{}, 37 | Where: sq. 38 | And(col.ID, sq.Equal(userID)), 39 | Review: "TODO", 40 | Limit: 1, 41 | }) 42 | if err != nil { 43 | return 44 | } 45 | // 你还可以通过 db.HardDelete() 永久删除数据 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /example/internal/insert/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | connectMysql "github.com/goclub/sql/example/internal/db" 7 | m "github.com/goclub/sql/example/internal/model" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | err := example(ctx) 14 | if err != nil { 15 | log.Print(err) 16 | } 17 | } 18 | func example(ctx context.Context) (err error) { 19 | db := connectMysql.DB 20 | // qb 是 goclub/sql 的核心,用于生成sql 21 | col := m.TableUser{}.Column() 22 | qb := sq.QB{ 23 | // From 用来配置表名和软删字段 24 | // From 可以使用 &m.User{} 或者 &m.TableUser{}, 它们两个都是通过 https://goclub.run/?k=model 生成的 25 | From: &m.TableUser{}, 26 | Insert: sq.Values{ 27 | {col.Name, "nimo"}, 28 | {col.Mobile, "1341111222"}, 29 | {col.ChinaIDCardNo, "31111119921219000"}, 30 | }, 31 | // Review 的作用是用于审查 sql 或增加代码可读性,可以忽略 32 | Review: "INSERT INTO `user` (`name`,`mobile`,`china_id_card_no`) VALUES (?,?,?)", 33 | } 34 | _, err = db.Insert(ctx, qb) 35 | if err != nil { 36 | // 无法处理的错误应当向上传递 37 | return err 38 | } 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /example/internal/insert_model/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | connectMysql "github.com/goclub/sql/example/internal/db" 7 | m "github.com/goclub/sql/example/internal/model" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | err := example(ctx) 14 | if err != nil { 15 | log.Print(err) 16 | } 17 | } 18 | func example(ctx context.Context) (err error) { 19 | db := connectMysql.DB 20 | user := m.User{ 21 | Name: "nimo", 22 | Mobile: "1341111222", 23 | ChinaIDCardNo: "31111119921219000", 24 | } 25 | // InsertModel 会自动获取 user 的结构体字段(struct field),会忽略 struct tag 中带有 sq:"ignoreInsert" 的字段 26 | // 使用 InsertModel 时 sq.QB 不需要配置 Form 27 | err = db.InsertModel(ctx, &user, sq.QB{ 28 | // Review 的作用是用于审查 sql 或增加代码可读性,可以忽略 29 | Review: "INSERT INTO `user` (`name`,`mobile`,`china_id_card_no`,`created_at`,`updated_at`) VALUES (?,?,?,?,?)", 30 | }) 31 | if err != nil { 32 | return 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /example/internal/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/go-sql-driver/mysql" 5 | xerr "github.com/goclub/error" 6 | sq "github.com/goclub/sql" 7 | "github.com/goclub/sql/example/internal/migrate/migrate" 8 | ) 9 | 10 | func main() { 11 | db, dbClose, err := sq.Open("mysql", sq.MysqlDataSource{ 12 | // 生产环境请使用环境变量或者配置中心配置数据库地址,不要硬编码在代码中 13 | User: "root", 14 | Password: "somepass", 15 | Host: "127.0.0.1", 16 | Port: "3306", 17 | DB: "example_goclub_sql", 18 | Query: map[string]string{ 19 | "charset": "utf8", 20 | "parseTime": "True", 21 | "loc": "Local", 22 | }, 23 | }.FormatDSN()) 24 | if err != nil { 25 | panic(err) 26 | } 27 | _ = dbClose() 28 | // 将包含 MigrateXXX(mi sq.Migrate) 方法的结构体指针传入 sq.ExecMigrate() 29 | if err = sq.ExecMigrate(db, &migrate.Migrate{db}); err != nil { 30 | xerr.PrintStack(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/internal/migrate/migrate/20201004160444_user.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | ) 7 | 8 | type Migrate struct { 9 | *sq.Database 10 | } 11 | 12 | func (dep Migrate) Migrate20201004160444CreateUserTable() (err error) { 13 | if _, err = dep.Exec(context.TODO(), ` 14 | CREATE TABLE user ( 15 | id char(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 16 | name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 17 | age int(11) NOT NULL DEFAULT '0', 18 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 20 | deleted_at timestamp NULL DEFAULT NULL, 21 | PRIMARY KEY (id), 22 | KEY name (name) 23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`, nil); err != nil { 24 | return 25 | } 26 | if _, err = dep.Exec(context.TODO(), ` 27 | CREATE TABLE user_address ( 28 | user_id char(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 29 | address varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 30 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 32 | deleted_at timestamp NULL DEFAULT NULL, 33 | PRIMARY KEY (user_id) 34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`, nil); err != nil { 35 | return 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /example/internal/model/user.go: -------------------------------------------------------------------------------- 1 | // Generate by https://goclub.run 2 | package m 3 | 4 | import ( 5 | sq "github.com/goclub/sql" 6 | ) 7 | 8 | type IDUser uint64 9 | 10 | func NewIDUser(id uint64) IDUser { 11 | return IDUser(id) 12 | } 13 | func (id IDUser) Uint64() uint64 { 14 | return uint64(id) 15 | } 16 | 17 | type TableUser struct { 18 | sq.SoftDeletedAt 19 | } 20 | 21 | // 给 TableName 加上指针 * 能避免 db.InsertModel(user) 这种错误, 应当使用 db.InsertModel(&user) 或 22 | func (*TableUser) TableName() string { return "user" } 23 | 24 | type User struct { 25 | ID IDUser `db:"id" sq:"ignoreInsert"` 26 | Name string `db:"name"` 27 | Mobile string `db:"mobile"` 28 | ChinaIDCardNo string `db:"china_id_card_no"` 29 | TableUser 30 | sq.CreatedAtUpdatedAt 31 | sq.DefaultLifeCycle 32 | } 33 | 34 | func (v *User) AfterInsert(result sq.Result) error { 35 | id, err := result.LastInsertUint64Id() 36 | if err != nil { 37 | return err 38 | } 39 | v.ID = NewIDUser(id) 40 | return nil 41 | } 42 | 43 | func (v TableUser) Column() (col struct { 44 | ID sq.Column 45 | Name sq.Column 46 | Mobile sq.Column 47 | ChinaIDCardNo sq.Column 48 | CreatedAt sq.Column 49 | UpdatedAt sq.Column 50 | }) { 51 | col.ID = "id" 52 | col.Name = "name" 53 | col.Mobile = "mobile" 54 | col.ChinaIDCardNo = "china_id_card_no" 55 | col.CreatedAt = "created_at" 56 | col.UpdatedAt = "updated_at" 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /example/internal/model/user_address.go: -------------------------------------------------------------------------------- 1 | // Generate by https://goclub.run 2 | package m 3 | 4 | import ( 5 | sq "github.com/goclub/sql" 6 | ) 7 | 8 | type TableUserAddress struct { 9 | sq.WithoutSoftDelete 10 | } 11 | 12 | // 给 TableName 加上指针 * 能避免 db.InsertModel(user) 这种错误, 应当使用 db.InsertModel(&user) 或 13 | func (*TableUserAddress) TableName() string { return "user_address" } 14 | 15 | type UserAddress struct { 16 | UserID IDUser `db:"user_id"` 17 | Address string `db:"address"` 18 | TableUserAddress 19 | sq.CreatedAtUpdatedAt 20 | sq.DefaultLifeCycle 21 | } 22 | 23 | func (v UserAddress) PrimaryKey() []sq.Condition { 24 | return sq.And( 25 | v.Column().UserID, sq.Equal(v.UserID), 26 | ) 27 | } 28 | 29 | func (v TableUserAddress) Column() (col struct { 30 | UserID sq.Column 31 | Address sq.Column 32 | CreatedAt sq.Column 33 | UpdatedAt sq.Column 34 | }) { 35 | col.UserID = "user_id" 36 | col.Address = "address" 37 | col.CreatedAt = "created_at" 38 | col.UpdatedAt = "updated_at" 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /example/internal/model/user_with_address.go: -------------------------------------------------------------------------------- 1 | package m 2 | 3 | import ( 4 | sq "github.com/goclub/sql" 5 | ) 6 | 7 | type UserWithAddress struct { 8 | UserID IDUser `db:"user.id"` 9 | Name string `db:"user.name"` 10 | Mobile string `db:"user.mobile"` 11 | ChinaIDCardNo string `db:"user.china_id_card_no"` 12 | Address string `db:"user_address.address"` 13 | } 14 | 15 | func (a UserWithAddress) SoftDeleteWhere() sq.Raw { 16 | return sq.Raw{"`user`.`deleted_at` IS NULL", nil} 17 | } 18 | 19 | func (a UserWithAddress) RelationJoin() []sq.Join { 20 | return []sq.Join{ 21 | { 22 | Type: sq.LeftJoin, 23 | TableName: "user_address", 24 | On: "`user`.`id` = `user_address`.`user_id`", 25 | }, 26 | } 27 | } 28 | 29 | func (UserWithAddress) TableName() string { 30 | return "user" 31 | } 32 | 33 | func (v UserWithAddress) Column() (col struct { 34 | UserID sq.Column 35 | Name sq.Column 36 | Mobile sq.Column 37 | ChinaIDCardNo sq.Column 38 | Address sq.Column 39 | }) { 40 | col.UserID = "user.id" 41 | col.Name = "user.name" 42 | col.Mobile = "user.mobile" 43 | col.ChinaIDCardNo = "user.china_id_card_no" 44 | col.Address = "user_address.address" 45 | return 46 | } 47 | 48 | func buildCheck() { 49 | var v sq.Relation 50 | v = &UserWithAddress{} 51 | _ = v 52 | } 53 | -------------------------------------------------------------------------------- /example/internal/query/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | connectMysql "github.com/goclub/sql/example/internal/db" 7 | m "github.com/goclub/sql/example/internal/model" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | err := example(ctx) 14 | if err != nil { 15 | log.Print(err) 16 | } 17 | } 18 | func example(ctx context.Context) (err error) { 19 | db := connectMysql.DB 20 | col := m.TableUser{}.Column() 21 | // 通过 InsertModel 准备数据 22 | insertUser := m.User{ 23 | Name: "query mobile", 24 | Mobile: "13411122222", 25 | ChinaIDCardNo: "310113199912121111", 26 | } 27 | err = db.InsertModel(ctx, &insertUser, sq.QB{ 28 | UseInsertIgnoreInto: true, // 为了便于测试忽略重复插入 29 | }) 30 | if err != nil { 31 | return 32 | } 33 | userID := insertUser.ID 34 | // 基于 Model 查询 35 | user := m.User{} 36 | // Query 会自动分析 user 的结构体字段(struct field) 这样 sq.QB{}.Select 就可以省略了 37 | // sq.QB{}.Form 也会自动设置为 user 38 | hasUser, err := db.Query(ctx, &user, sq.QB{ 39 | Where: sq. 40 | And(col.ID, sq.Equal(userID)), 41 | Review: "SELECT `id`, `name`, `mobile`, `china_id_card_no`, `created_at`, `updated_at` FROM `user` WHERE `id` = ? AND `deleted_at` IS NULL LIMIT ?", 42 | }) 43 | if err != nil { 44 | return 45 | } 46 | log.Print("hasUser:", hasUser) 47 | log.Print("user:", user) 48 | // 基于 TableUser 查询部分数据 49 | type PartUser struct { 50 | m.TableUser // 组合 TableUser 可以快速配置表名和软删 51 | Name string `db:"name"` 52 | Mobile string `db:"mobile"` 53 | } 54 | partUser := PartUser{} 55 | hasPartUser, err := db.Query(ctx, &partUser, sq.QB{ 56 | Where: sq. 57 | And(col.ID, sq.Equal(userID)), 58 | Review: "SELECT `name`, `mobile` FROM `user` WHERE `id` = ? AND `deleted_at` IS NULL LIMIT ?", 59 | }) 60 | if err != nil { 61 | return 62 | } 63 | log.Print("hasPartUser:", hasPartUser) 64 | log.Print("partUser:", partUser) 65 | 66 | // QuerySlice 可以查询多条数据 67 | var userList []m.User 68 | err = db.QuerySlice(ctx, &userList, sq.QB{}) 69 | if err != nil { 70 | return 71 | } 72 | log.Print("userList:", userList) 73 | // 还可以使用 sq.QB{}.Paging(1, 10) 进行分页查询 74 | /* 75 | err = db.QuerySlice(ctx, &userList, sq.QB{}.Paging(1, 10)) ; if err != nil { 76 | return 77 | } 78 | */ 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /example/internal/relation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | connectMysql "github.com/goclub/sql/example/internal/db" 7 | m "github.com/goclub/sql/example/internal/model" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | err := example(ctx) 14 | if err != nil { 15 | log.Print(err) 16 | } 17 | } 18 | func example(ctx context.Context) (err error) { 19 | db := connectMysql.DB 20 | // 准备数据 21 | var insertUser m.User 22 | // 多表插入一定要用事务,否则无法保证数据一致性 23 | rollbackNoError, err := db.Begin(ctx, sq.LevelReadCommitted, func(tx *sq.T) sq.TxResult { 24 | db := false 25 | _ = db // 一般情况下事务中都是使用tx所以重新声明变量db 防止在 tx 中使用db 26 | insertUser = m.User{ 27 | Name: "relation mobile", 28 | Mobile: "13411122222", 29 | ChinaIDCardNo: "310113199912121112", 30 | } 31 | err = tx.InsertModel(ctx, &insertUser, sq.QB{ 32 | UseInsertIgnoreInto: true, // 为了便于测试忽略重复插入 33 | }) 34 | if err != nil { 35 | return tx.RollbackWithError(err) 36 | } 37 | err = tx.InsertModel(ctx, &m.UserAddress{ 38 | UserID: insertUser.ID, 39 | Address: "天堂路", 40 | }, sq.QB{ 41 | UseInsertIgnoreInto: true, // 为了便于测试忽略重复插入 42 | }) 43 | if err != nil { 44 | return tx.RollbackWithError(err) 45 | } 46 | return tx.Commit() 47 | }) 48 | if err != nil { 49 | return 50 | } 51 | if rollbackNoError { 52 | // 运行到 Begin 中的 return tx.Rollback() 时, rollbackNoError 为 true 53 | } 54 | userWithAddress := m.UserWithAddress{} 55 | col := userWithAddress.Column() 56 | hasUserWithAddress, err := db.QueryRelation(ctx, &userWithAddress, sq.QB{ 57 | Where: sq.And(col.UserID, sq.Equal(insertUser.ID)), 58 | }) 59 | if err != nil { 60 | return 61 | } 62 | log.Print("hasUserWithAddress:", hasUserWithAddress) 63 | log.Print("userWithAddress:", userWithAddress) 64 | // QueryRelationSlice 查询多条关联数据 65 | var userWithAddressList []m.UserWithAddress 66 | err = db.QueryRelationSlice(ctx, &userWithAddressList, sq.QB{}) 67 | if err != nil { 68 | return 69 | } 70 | log.Print("userWithAddressList:", userWithAddressList) 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /example/internal/update/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | connectMysql "github.com/goclub/sql/example/internal/db" 7 | m "github.com/goclub/sql/example/internal/model" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | err := example(ctx) 14 | if err != nil { 15 | log.Print(err) 16 | } 17 | } 18 | func example(ctx context.Context) (err error) { 19 | db := connectMysql.DB 20 | col := m.TableUser{}.Column() 21 | // qb 是 goclub/sql 的核心,用于生成sql 22 | qb := sq.QB{ 23 | From: &m.TableUser{}, 24 | // 可以使用 sq.SetMap/sq.Set/sq.SetRaw 25 | Set: sq.SetMap(map[sq.Column]interface{}{ 26 | col.Name: "tim", 27 | col.Mobile: "13022228888", 28 | }), 29 | Where: sq.And(col.ID, sq.Equal("1514f086-692e-4666-8bfd-3052d1b51261")), 30 | // Review 的作用是用于审查 sql 或增加代码可读性,可以忽略 31 | Review: "UPDATE `user` SET `mobile`= ?,`name`= ? WHERE `id` = ? AND `deleted_at` IS NULL", 32 | } 33 | affected, err := db.UpdateAffected(ctx, &m.TableUser{}, qb) 34 | if err != nil { 35 | // 无法处理的错误应当向上传递 36 | return 37 | } 38 | if err != nil { 39 | return 40 | } 41 | log.Print("affected:", affected) 42 | 43 | // 你可以直接简写成 44 | // affected, err := sq.RowsAffected(db.Set(ctx, qb)) ; if err != nil { 45 | // return 46 | // } 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /generics/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goclub/sql/generics 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /generics/query.go: -------------------------------------------------------------------------------- 1 | package sqg 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goclub/sql 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.6.0 7 | github.com/goclub/error v0.0.0-20210713081010-9481fb4c4922 8 | github.com/google/uuid v1.1.4 9 | github.com/jaevor/go-nanoid v1.3.0 10 | github.com/jedib0t/go-pretty/v6 v6.2.4 11 | github.com/jmoiron/sqlx v1.3.4 12 | github.com/stretchr/testify v1.8.0 13 | go.uber.org/zap v1.24.0 14 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= 7 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 8 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 9 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 10 | github.com/goclub/error v0.0.0-20210713081010-9481fb4c4922 h1:tvyn7G9QsvuCj02Ja+cxlIkf4jdo/FXOG8duqd9fDVI= 11 | github.com/goclub/error v0.0.0-20210713081010-9481fb4c4922/go.mod h1:2ombCAL3uNuhVNevp56iOpelgiQIyxlYkz7t6nWEC18= 12 | github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0= 13 | github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg= 15 | github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY= 16 | github.com/jedib0t/go-pretty/v6 v6.2.4 h1:wdaj2KHD2W+mz8JgJ/Q6L/T5dB7kyqEFI16eLq7GEmk= 17 | github.com/jedib0t/go-pretty/v6 v6.2.4/go.mod h1:+nE9fyyHGil+PuISTCrp7avEdo6bqoMwqZnuiK2r2a0= 18 | github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= 19 | github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= 20 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 21 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 22 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 23 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 24 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 25 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 26 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 27 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 28 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 29 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 30 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 31 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 32 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 33 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 39 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 45 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 46 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 47 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 48 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 49 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 50 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 51 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 52 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 53 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 54 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 55 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 56 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 57 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 58 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 59 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 60 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 61 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 62 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 63 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 64 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 65 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 69 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 73 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 79 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 80 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 81 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 82 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 85 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 89 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 91 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 92 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 93 | -------------------------------------------------------------------------------- /id.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | xerr "github.com/goclub/error" 5 | "github.com/google/uuid" 6 | "github.com/jaevor/go-nanoid" 7 | "strings" 8 | ) 9 | 10 | func UUID() string { 11 | return uuid.New().String() 12 | } 13 | func UUID32() string { 14 | return strings.ReplaceAll(UUID(), "-", "") 15 | } 16 | func init() { 17 | var err error 18 | newNanoid, err = nanoid.Custom("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 24) // indivisible begin 19 | if err != nil { // indivisible end 20 | panic(xerr.WrapPrefix("unexpected", err)) 21 | } 22 | } 23 | 24 | var newNanoid = func() string { 25 | panic("unexpected") 26 | } 27 | 28 | // NanoID24 `A-Za-z0-9` 24 29 | // 某些第三方接口需要外部订单号是大小写字母加数字,所以用`A-Za-z0-9` 24 比 默认的21更稳妥. 30 | func NanoID24() string { 31 | return newNanoid() 32 | } 33 | -------------------------------------------------------------------------------- /incr_decr.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | // 递增存在并发问题,现在不满意目前设计的接口,等以后在想怎么设计 2021年02月02日10:32:15 @nimoc。 4 | // // 递减可能存在并发问题,所以只在事务中处理 5 | // func (tx *T) DecrementIntModel(ctx context.Context, ptr Model, props IncrementInt, checkSQL ...string) (affected bool, err error) { 6 | // field := props.Column.wrapField() 7 | // result, err := coreUpdateModel(ctx, tx.Core, ptr, []Data{ 8 | // { 9 | // // SET age = age - ? 10 | // Raw: Raw{ 11 | // Query: field + " = " + field + " - ?", 12 | // Values: []interface{}{props.Value}, 13 | // }, 14 | // OnUpdated: func() error { 15 | // return props.OnUpdated(props.Value) 16 | // }, 17 | // }, 18 | // }, []Condition{ 19 | // ConditionRaw( 20 | // // WHERE age >= stock 21 | // field + " >= " + props.AfterIncrementLessThanOrEqual.wrapField(), 22 | // []interface{}{props.Value}, 23 | // ), 24 | // }) 25 | // if err != nil { 26 | // return 27 | // } 28 | // rowsAffected, err := result.RowsAffected() ; if err != nil { 29 | // return 30 | // } 31 | // affected = rowsAffected !=0 32 | // return 33 | // } 34 | // type DecrementFloat struct { 35 | // Column Column 36 | // Value float64 37 | // AfterDecrementGreaterThanOrEqual Column 38 | // OnUpdated func(value float64) error 39 | // } 40 | // // 递减可能存在并发问题,所以只在事务中处理 41 | // func (tx *T) DecrementFloatModel(ctx context.Context, ptr Model, props IncrementFloat, checkSQL ...string) (affected bool, err error) { 42 | // field := props.Column.wrapField() 43 | // result, err := coreUpdateModel(ctx, tx.Core, ptr, []Data{ 44 | // { 45 | // // SET age = age - ? 46 | // Raw: Raw{ 47 | // Query: field + " = " + field + " - ?", 48 | // Values: []interface{}{props.Value}, 49 | // }, 50 | // OnUpdated: func() error { 51 | // return props.OnUpdated(props.Value) 52 | // }, 53 | // }, 54 | // }, []Condition{ 55 | // ConditionRaw( 56 | // // WHERE age >= stock 57 | // field + " >= " + props.AfterIncrementLessThanOrEqual.wrapField(), 58 | // []interface{}{props.Value}, 59 | // ), 60 | // }) 61 | // if err != nil { 62 | // return 63 | // } 64 | // rowsAffected, err := result.RowsAffected() ; if err != nil { 65 | // return 66 | // } 67 | // affected = rowsAffected !=0 68 | // return 69 | // } 70 | // type DecrementInt struct { 71 | // Column Column 72 | // Value uint 73 | // AfterDecrementGreaterThanOrEqual Column 74 | // OnUpdated func(value uint) error 75 | // } 76 | // type IncrementInt struct { 77 | // Column Column 78 | // Value uint 79 | // AfterIncrementLessThanOrEqual Column 80 | // OnUpdated func(value uint) error 81 | // } 82 | // // 递增可能存在并发问题,所以只在事务中处理 83 | // func (tx *T) IncrementIntModel(ctx context.Context, ptr Model, props IncrementInt) (affected bool, err error) { 84 | // field := props.Column.wrapField() 85 | // result, err := db.UpdateModel(ctx, ptr, []Data{ 86 | // { 87 | // // SET age = age + ? 88 | // Raw: Raw{ 89 | // Query: field + " = " + field + " + ?", 90 | // Values: []interface{}{props.Value}, 91 | // }, 92 | // OnUpdated: func() error { 93 | // return props.OnUpdated(props.Value) 94 | // }, 95 | // }, 96 | // }, []Condition{ 97 | // ConditionRaw( 98 | // // WHERE age + ? <= stock 99 | // field + " + ? <= " + props.AfterIncrementLessThanOrEqual.wrapField(), 100 | // []interface{}{props.Value}, 101 | // ), 102 | // }) 103 | // if err != nil { 104 | // return 105 | // } 106 | // rowsAffected, err := result.RowsAffected() ; if err != nil { 107 | // return 108 | // } 109 | // affected = rowsAffected !=0 110 | // return 111 | // } 112 | // type IncrementFloat struct { 113 | // Column Column 114 | // Value float64 115 | // AfterIncrementLessThanOrEqual Column 116 | // OnUpdated func(value float64) error 117 | // } 118 | // func (tx *T) IncrementFloatModel(ctx context.Context, ptr Model, props IncrementFloat) (affected bool, err error) { 119 | // field := props.Column.wrapField() 120 | // result, err := tx.UpdateModel(ctx, ptr, []Data{ 121 | // { 122 | // // SET age = age + ? 123 | // Raw: Raw{ 124 | // Query: field + " = " + field + " + ?", 125 | // Values: []interface{}{props.Value}, 126 | // }, 127 | // OnUpdated: func() error { 128 | // return props.OnUpdated(props.Value) 129 | // }, 130 | // }, 131 | // }, []Condition{ 132 | // ConditionRaw( 133 | // // WHERE age + ? <= stock 134 | // field + " + ? <= " + props.AfterIncrementLessThanOrEqual.wrapField(), 135 | // []interface{}{props.Value}, 136 | // ), 137 | // }) 138 | // if err != nil { 139 | // return 140 | // } 141 | // rowsAffected, err := result.RowsAffected() ; if err != nil { 142 | // return 143 | // } 144 | // affected = rowsAffected !=0 145 | // return 146 | // } 147 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "github.com/jmoiron/sqlx" 6 | ) 7 | 8 | // sq.Table("user",nil, nil) 9 | // sq.Table("user", sq.Raw{"`deleted_at` IS NULL", nil}, sq.Raw{"`deleted_at` = ?" ,[]interface{}{time.Now()}}) 10 | func Table(tableName string, softDeleteWhere func() Raw, softDeleteSet func() Raw) Tabler { 11 | if softDeleteWhere == nil { 12 | softDeleteWhere = func() Raw { 13 | return Raw{} 14 | } 15 | } 16 | if softDeleteSet == nil { 17 | softDeleteSet = func() Raw { 18 | return Raw{} 19 | } 20 | } 21 | return table{ 22 | tableName: tableName, 23 | softDeleteWhere: softDeleteWhere, 24 | softDeleteSet: softDeleteSet, 25 | } 26 | } 27 | 28 | type Tabler interface { 29 | TableName() string 30 | SoftDeleteWhere() Raw 31 | SoftDeleteSet() Raw 32 | } 33 | 34 | // 供 relation sq.Table() 使用 35 | type table struct { 36 | tableName string 37 | softDeleteWhere func() Raw 38 | softDeleteSet func() Raw 39 | } 40 | 41 | func (t table) TableName() string { 42 | return t.tableName 43 | } 44 | func (t table) SoftDeleteWhere() Raw { 45 | return t.softDeleteWhere() 46 | } 47 | func (t table) SoftDeleteSet() Raw { 48 | return t.softDeleteSet() 49 | } 50 | 51 | type Raw struct { 52 | Query string 53 | Values []interface{} 54 | } 55 | 56 | func (r Raw) IsZero() bool { 57 | return r.Query == "" 58 | } 59 | 60 | type Model interface { 61 | Tabler 62 | BeforeInsert() error 63 | AfterInsert(result Result) error 64 | BeforeUpdate() error 65 | AfterUpdate() error 66 | } 67 | type Relation interface { 68 | TableName() string 69 | SoftDeleteWhere() Raw 70 | RelationJoin() []Join 71 | } 72 | 73 | type DefaultLifeCycle struct { 74 | } 75 | 76 | func (v *DefaultLifeCycle) BeforeInsert() error { return nil } 77 | func (v *DefaultLifeCycle) AfterInsert(result Result) error { return nil } 78 | func (v *DefaultLifeCycle) BeforeUpdate() error { return nil } 79 | func (v *DefaultLifeCycle) AfterUpdate() error { return nil } 80 | 81 | type Storager interface { 82 | getCore() StoragerCore 83 | getSQLChecker() SQLChecker 84 | } 85 | type StoragerCore interface { 86 | sqlx.Queryer 87 | sqlx.QueryerContext 88 | sqlx.Execer 89 | sqlx.ExecerContext 90 | sqlx.Preparer 91 | sqlx.PreparerContext 92 | SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error 93 | } 94 | 95 | type sqlInsertRawer interface { 96 | SQLInsertRaw() (query string, values []interface{}) 97 | } 98 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | zap "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | "log" 7 | "os" 8 | "runtime/debug" 9 | ) 10 | 11 | type Logger interface { 12 | Debug(message string, keysAndValues ...interface{}) 13 | Info(message string, keysAndValues ...interface{}) 14 | Warn(message string, keysAndValues ...interface{}) 15 | Error(message string, keysAndValues ...interface{}) 16 | Sync() error 17 | } 18 | 19 | var Log = NewZapLogger() 20 | 21 | type DefaultLog struct { 22 | core *zap.SugaredLogger 23 | } 24 | 25 | func NewZapLogger() Logger { 26 | // 编码 27 | encoderConfig := zap.NewProductionEncoderConfig() 28 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 29 | encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 30 | encoder := zapcore.NewConsoleEncoder(encoderConfig) 31 | 32 | core := zapcore.NewCore( 33 | encoder, 34 | zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)), 35 | zap.LevelEnablerFunc(func(curentLevel zapcore.Level) bool { 36 | return true 37 | }), 38 | ) 39 | 40 | options := []zap.Option{ 41 | zap.AddCaller(), 42 | // zap.AddStacktrace(zapcore.DebugLevel), 43 | // zap.AddStacktrace(zapcore.InfoLevel), 44 | zap.AddStacktrace(zapcore.WarnLevel), 45 | zap.AddStacktrace(zapcore.ErrorLevel), 46 | zap.AddCallerSkip(3), 47 | } 48 | 49 | return &DefaultLog{ 50 | core: zap.New(core, options...).Sugar(), 51 | } 52 | } 53 | func (z DefaultLog) log(fn func(msg string, keysAndValues ...interface{}), msg string, keysAndValues ...interface{}) { 54 | defer func() { 55 | r := recover() 56 | if r != nil { 57 | log.Print(r) 58 | debug.PrintStack() 59 | } 60 | }() 61 | fn(msg, keysAndValues...) 62 | } 63 | func (z DefaultLog) Debug(message string, keysAndValues ...interface{}) { 64 | z.log(z.core.Debugw, message, keysAndValues...) 65 | } 66 | func (z DefaultLog) Info(message string, keysAndValues ...interface{}) { 67 | z.log(z.core.Infow, message, keysAndValues...) 68 | } 69 | func (z DefaultLog) Warn(message string, keysAndValues ...interface{}) { 70 | z.log(z.core.Warnw, message, keysAndValues...) 71 | } 72 | func (z DefaultLog) Error(message string, keysAndValues ...interface{}) { 73 | z.log(z.core.Errorw, message, keysAndValues...) 74 | } 75 | func (z DefaultLog) Panic(message string, keysAndValues ...interface{}) { 76 | z.log(z.core.Panicw, message, keysAndValues...) 77 | } 78 | func (z DefaultLog) DPanic(message string, keysAndValues ...interface{}) { 79 | z.log(z.core.DPanicw, message, keysAndValues...) 80 | } 81 | func (z DefaultLog) Fatal(message string, keysAndValues ...interface{}) { 82 | z.log(z.core.Fatalw, message, keysAndValues...) 83 | } 84 | func (z DefaultLog) Sync() error { 85 | return z.core.Sync() 86 | } 87 | -------------------------------------------------------------------------------- /media/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goclub/sql/f7eb6e53885ae3d56042bcb058028e4b008b18b6/media/debug.png -------------------------------------------------------------------------------- /migrate.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | xerr "github.com/goclub/error" 6 | "log" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | const createMigratestringQueueL = ` 12 | CREATE TABLE IF NOT EXISTS goclub_sql_migrations ( 13 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 14 | name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 15 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | PRIMARY KEY (id), 17 | UNIQUE KEY name (name) 18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 19 | ` 20 | 21 | func ExecMigrate(db *Database, ptr interface{}) (err error) { 22 | ctx := context.Background() 23 | table := Table("goclub_sql_migrations", nil, nil) 24 | rPtrValue := reflect.ValueOf(ptr) 25 | if rPtrValue.Kind() != reflect.Ptr { 26 | panic(xerr.New("ExecMigrate(db, ptr) ptr must be pointer")) 27 | } 28 | rValue := rPtrValue.Elem() 29 | rType := rValue.Type() 30 | if _, err = db.Exec(ctx, createMigratestringQueueL, nil); err != nil { 31 | return 32 | } 33 | methodNames := []string{} 34 | for i := 0; i < rType.NumMethod(); i++ { 35 | method := rType.Method(i) 36 | if strings.HasPrefix(method.Name, "Migrate") { 37 | methodNames = append(methodNames, method.Name) 38 | } 39 | } 40 | for _, methodName := range methodNames { 41 | var has bool 42 | if has, err = db.Has(ctx, table, QB{ 43 | Where: And("name", Equal(methodName)), 44 | }); err != nil { 45 | return 46 | } 47 | if has { 48 | continue 49 | } 50 | log.Print("[goclub_sql migrate]exec: " + methodName) 51 | out := rValue.MethodByName(methodName).Call([]reflect.Value{}) 52 | if len(out) != 1 { 53 | return xerr.New(methodName + "() must return error or nil") 54 | } 55 | errOrNil := out[0].Interface() 56 | if errOrNil != nil { 57 | return errOrNil.(error) 58 | } 59 | if _, err = db.Insert(ctx, QB{ 60 | From: table, 61 | Insert: Values{ 62 | {"name", methodName}, 63 | }, 64 | }); err != nil { 65 | return 66 | } 67 | log.Printf("[goclub_sql migrate]done: " + methodName) 68 | } 69 | return 70 | } 71 | -------------------------------------------------------------------------------- /migrate_test.go: -------------------------------------------------------------------------------- 1 | package sq_test 2 | 3 | import ( 4 | "context" 5 | sq "github.com/goclub/sql" 6 | ) 7 | 8 | type Migrate struct { 9 | db *sq.Database 10 | } 11 | 12 | func (dep Migrate) Migrate20201004160444CreateUserTable() (err error) { 13 | ctx := context.Background() 14 | if _, err = dep.db.Exec(ctx, ` 15 | CREATE TABLE user ( 16 | id char(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 17 | name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 18 | age int(11) NOT NULL DEFAULT '0', 19 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 21 | deleted_at timestamp NULL DEFAULT NULL, 22 | PRIMARY KEY (id), 23 | KEY name (name) 24 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 25 | `, nil); err != nil { 26 | return 27 | } 28 | if _, err = dep.db.Exec(ctx, ` 29 | CREATE TABLE user_address ( 30 | user_id char(36) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 31 | address varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', 32 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 34 | deleted_at timestamp NULL DEFAULT NULL, 35 | PRIMARY KEY (user_id) 36 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 37 | `, nil); err != nil { 38 | return 39 | } 40 | if _, err = dep.db.Exec(ctx, ` 41 | CREATE TABLE `+"`insert`"+` ( 42 | id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 43 | age int(11) DEFAULT NULL, 44 | created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 45 | updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 46 | deleted_at timestamp NULL DEFAULT NULL, 47 | PRIMARY KEY (id) 48 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 49 | `, nil); err != nil { 50 | return 51 | } 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /model_test.go: -------------------------------------------------------------------------------- 1 | package sq_test 2 | 3 | import ( 4 | "database/sql" 5 | sq "github.com/goclub/sql" 6 | ) 7 | 8 | // 当有一张 user 表 9 | // id name age created_at updated_at deleted_at 10 | // 7d96ba00-f788-4b2c-86d6-d71d3b41c903 nimo 18 2021-01-06 03:37:36 2021-01-06 03:38:01 NULL 11 | // 根据表的信息写出如下类型 12 | 13 | // 定义符合 sq.Tabler 接口的结构体 14 | type TableUser struct { 15 | // 通过 goclub/sql 提供的 SoftDeleteDeletedAt 表明该表存在软删字段,还可以使用 sq.SoftDeleteIsDeleted sq.SoftDeleteDeleteTime 16 | // 它们的功能是让 TableUser 支持 SoftDeleteWhere() SoftDeleteSet() 方法 17 | sq.SoftDeletedAt 18 | } 19 | 20 | // 通过 TableName() 配置表名 21 | func (*TableUser) TableName() string { return "user" } 22 | 23 | // 给 user 表的 id 字段增加类型可减少代码中传错 id 的错误 24 | type IDUser string 25 | 26 | // 定义符合 sq.Model 接口的结构体 27 | type User struct { 28 | ID IDUser `db:"id"` 29 | Name string `db:"name"` 30 | Age int `db:"age"` 31 | // CreatedAtUpdatedAt 表明表是支持 created_at 和 updated_at 字段的,还可以使用 sq.CreateTimeUpdateTime sq.GMTCreateGMTUpdate 32 | sq.CreatedAtUpdatedAt 33 | // 通过组合 TableUser 让 User 支持 TableName() SoftDeleteWhere() SoftDeleteSet() 等方法 34 | TableUser 35 | // 每个 Model 都应该具有生命周期触发函数 BeforeInsert() AfterInsert() BeforeUpdate() AfterUpdate() 方法 36 | // 通过 sq.DefaultLifeCycle 可配置默认的生命周期触发函数 37 | sq.DefaultLifeCycle 38 | } 39 | 40 | func (u User) PrimaryKey() []sq.Condition { 41 | return sq.And(u.Column().ID, sq.Equal(u.ID)) 42 | } 43 | 44 | // 因为 user 表的 id 字段是 uuid,所以在 User 的 BeforeInsert 生命周期去创建 id 45 | func (u *User) BeforeInsert() error { 46 | if len(u.ID) == 0 { 47 | u.ID = IDUser(sq.UUID()) 48 | } 49 | return nil 50 | } 51 | 52 | // 为了避免在代码中重复的写 "id" "name" "age" 等字符串时候写错单词导致的错误,实现 Column 方法避免出错。 53 | func (User) Column() (col struct { 54 | ID sq.Column 55 | Name sq.Column 56 | Age sq.Column 57 | }) { 58 | col.ID = "id" 59 | col.Name = "name" 60 | col.Age = "age" 61 | return 62 | } 63 | 64 | type UserWithAddress struct { 65 | UserID IDUser `db:"user.id"` 66 | Name string `db:"user.name"` 67 | Age int `db:"user.age"` 68 | Address sql.NullString `db:"user_address.address"` 69 | } 70 | 71 | func (UserWithAddress) SoftDeleteWhere() sq.Raw { 72 | return sq.Raw{"`user`.`deleted_at` IS NULL AND `user_address`.`deleted_at` IS NULL", nil} 73 | } 74 | func (*UserWithAddress) TableName() string { return "user" } 75 | func (UserWithAddress) RelationJoin() []sq.Join { 76 | return []sq.Join{ 77 | { 78 | Type: sq.LeftJoin, 79 | TableName: "user_address", 80 | On: "`user`.`id` = `user_address`.`user_id`", 81 | }, 82 | } 83 | } 84 | 85 | type TableUserAddress struct { 86 | // 通过 goclub/sql 提供的 SoftDeleteDeletedAt 表明该表存在软删字段,还可以使用 sq.SoftDeleteIsDeleted sq.SoftDeleteDeleteTime 87 | // 它们的功能是让 TableUser 支持 SoftDeleteWhere() SoftDeleteSet() 方法 88 | sq.SoftDeletedAt 89 | } 90 | type UserAddress struct { 91 | UserID IDUser `db:"user_id"` 92 | Address string `db:"address"` 93 | // CreatedAtUpdatedAt 表明表是支持 created_at 和 updated_at 字段的,还可以使用 sq.CreateTimeUpdateTime sq.GMTCreateGMTUpdate 94 | sq.CreatedAtUpdatedAt 95 | // 通过组合 TableUser 让 User 支持 TableName() SoftDeleteWhere() SoftDeleteSet() 等方法 96 | TableUserAddress 97 | // 每个 Model 都应该具有生命周期触发函数 BeforeInsert() AfterInsert() BeforeUpdate() AfterUpdate() 方法 98 | // 通过 sq.DefaultLifeCycle 可配置默认的生命周期触发函数 99 | sq.DefaultLifeCycle 100 | } 101 | 102 | func (UserAddress) TableName() string { 103 | return "user_address" 104 | } 105 | func (UserWithAddress) Column() (col struct { 106 | UserID sq.Column 107 | Name sq.Column 108 | Age sq.Column 109 | Address sq.Column 110 | }) { 111 | col.UserID = "user.id" 112 | col.Name = "user.name" 113 | col.Age = "user.age" 114 | col.Address = "user_address.user_id" 115 | return 116 | } 117 | -------------------------------------------------------------------------------- /op.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | xerr "github.com/goclub/error" 5 | "reflect" 6 | ) 7 | 8 | type OP struct { 9 | Query string 10 | Values []interface{} 11 | Symbol string 12 | Placeholder string 13 | Multiple []OP 14 | OrGroup []Condition 15 | Ignore bool 16 | } 17 | 18 | func (op OP) sql(column Column, values *[]interface{}) string { 19 | var and stringQueue 20 | if len(op.OrGroup) != 0 { 21 | raw := conditions(op.OrGroup).coreSQL("OR") 22 | if raw.Query == "" { 23 | return "" 24 | } 25 | raw.Query = "(" + raw.Query + ")" 26 | *values = append(*values, raw.Values...) 27 | return raw.Query 28 | } else if len(op.Multiple) != 0 { 29 | for _, subOP := range op.Multiple { 30 | and.Push(subOP.sql(column, values)) 31 | } 32 | } else { 33 | if len(op.Query) != 0 { 34 | and.Push(op.Query) 35 | *values = append(*values, op.Values...) 36 | } else { 37 | and.Push(column.wrapField()) 38 | and.Push(op.Symbol) 39 | if len(op.Placeholder) != 0 { 40 | and.Push(op.Placeholder) 41 | } else { 42 | and.Push(sqlPlaceholder) 43 | } 44 | *values = append(*values, op.Values...) 45 | } 46 | } 47 | return and.Join(" ") 48 | } 49 | func Equal(v interface{}) OP { 50 | return OP{ 51 | Symbol: "=", 52 | Values: []interface{}{v}, 53 | } 54 | } 55 | func NotEqual(v interface{}) OP { 56 | return OP{ 57 | Symbol: "<>", 58 | Values: []interface{}{v}, 59 | } 60 | } 61 | func SubQuery(symbol string, qb QB) OP { 62 | raw := qb.SQLSelect() 63 | query, values := raw.Query, raw.Values 64 | return OP{ 65 | Placeholder: "(" + query + ")", 66 | Symbol: symbol, 67 | Values: values, 68 | } 69 | } 70 | func Like(s string) OP { 71 | return OP{ 72 | Symbol: "LIKE", 73 | Values: []interface{}{"%" + s + "%"}, 74 | } 75 | } 76 | func In(slice interface{}) OP { 77 | var placeholder string 78 | var values []interface{} 79 | rValue := reflect.ValueOf(slice) 80 | if rValue.Type().Kind() != reflect.Slice { 81 | panic(xerr.New("sq.In(" + rValue.Type().Name() + ") slice must be slice")) 82 | } 83 | if rValue.Len() == 0 { 84 | placeholder = "(NULL)" 85 | } else { 86 | var placeholderList stringQueue 87 | for i := 0; i < rValue.Len(); i++ { 88 | values = append(values, rValue.Index(i).Interface()) 89 | placeholderList.Push(sqlPlaceholder) 90 | } 91 | placeholder = "(" + placeholderList.Join(",") + ")" 92 | } 93 | return OP{ 94 | Symbol: "IN", 95 | Values: values, 96 | Placeholder: placeholder, 97 | } 98 | } 99 | func LikeLeft(s string) OP { 100 | return OP{ 101 | Symbol: "LIKE", 102 | Values: []interface{}{s + "%"}, 103 | } 104 | } 105 | func LikeRight(s string) OP { 106 | return OP{ 107 | Symbol: "LIKE", 108 | Values: []interface{}{"%" + s}, 109 | } 110 | } 111 | func Between(begin interface{}, end interface{}) OP { 112 | return OP{ 113 | Symbol: "BETWEEN", 114 | Values: []interface{}{begin, end}, 115 | Placeholder: `? AND ?`, 116 | } 117 | } 118 | func NotBetween(begin interface{}, end interface{}) OP { 119 | return OP{ 120 | Symbol: "NOT BETWEEN", 121 | Values: []interface{}{begin, end}, 122 | Placeholder: `? AND ?`, 123 | } 124 | } 125 | func GT(v interface{}) OP { 126 | return OP{ 127 | Symbol: ">", 128 | Values: []interface{}{v}, 129 | } 130 | } 131 | func GTE(v interface{}) OP { 132 | return OP{ 133 | Symbol: ">=", 134 | Values: []interface{}{v}, 135 | } 136 | } 137 | func LT(v interface{}) OP { 138 | return OP{ 139 | Symbol: "<", 140 | Values: []interface{}{v}, 141 | } 142 | } 143 | func LTE(v interface{}) OP { 144 | return OP{ 145 | Symbol: "<=", 146 | Values: []interface{}{v}, 147 | } 148 | } 149 | 150 | func IsNull() OP { 151 | return OP{ 152 | Symbol: "IS NULL", 153 | Values: nil, 154 | } 155 | } 156 | func Multiple(ops []OP) OP { 157 | return OP{ 158 | Multiple: ops, 159 | } 160 | } 161 | func IF(condition bool, op OP) OP { 162 | if condition == false { 163 | op.Ignore = true 164 | } 165 | return op 166 | } 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "doc": "fis3 release -d ./" 4 | } 5 | } -------------------------------------------------------------------------------- /query_builder.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | xerr "github.com/goclub/error" 7 | "math/big" 8 | "sort" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | // sq.Set(column, value) 15 | type Update struct { 16 | Column Column 17 | Value interface{} 18 | Raw Raw 19 | } 20 | type updates []Update 21 | 22 | func OnlyUseInTestToUpdates(t *testing.T, list []Update) updates { 23 | return list 24 | } 25 | func (u updates) Set(column Column, value interface{}) updates { 26 | if op, ok := value.(OP); ok { 27 | value = op.Values[0] 28 | Log.Warn(`sq.Set(` + column.String() + `, value) value can not be sq.Equal(v) or sq.OP{}, may be you need use like sq.Set("` + column.String() + `", v)`) 29 | } 30 | u = append(u, Update{ 31 | Column: column, 32 | Value: value, 33 | }) 34 | return u 35 | } 36 | func (u updates) SetRaw(query string, values ...interface{}) updates { 37 | for i, value := range values { 38 | if op, ok := value.(OP); ok { 39 | values[i] = op.Values[0] 40 | Log.Warn("sq.SetRaw(query, values) values element can not be sq.Equal(v) or sq.OP{}, may be you need use like sq.Set(\"id\", taskID)") 41 | } 42 | } 43 | u = append(u, Update{ 44 | Raw: Raw{query, values}, 45 | }) 46 | return u 47 | } 48 | func Set(column Column, value interface{}) updates { 49 | return updates{}.Set(column, value) 50 | } 51 | func SetMap(data map[Column]interface{}) updates { 52 | var list []Update 53 | for column, value := range data { 54 | list = append(list, Update{ 55 | Column: column, 56 | Value: value, 57 | }) 58 | } 59 | sort.Slice(list, func(i, j int) bool { 60 | return strings.Compare(list[i].Column.String(), list[j].Column.String()) == -1 61 | }) 62 | return list 63 | } 64 | func SetRaw(query string, value ...interface{}) updates { 65 | return updates{}.SetRaw(query, value...) 66 | } 67 | 68 | type InsertMultiple struct { 69 | Column []Column 70 | Values [][]interface{} 71 | } 72 | type Values []Insert 73 | 74 | type Insert struct { 75 | Column Column 76 | Value interface{} 77 | } 78 | 79 | type QB struct { 80 | Select []Column 81 | SelectRaw []Raw 82 | 83 | From Tabler 84 | from string 85 | FromRaw FromRaw 86 | 87 | DisableSoftDelete bool 88 | softDelete Raw 89 | 90 | UnionTable UnionTable 91 | 92 | Index string 93 | 94 | Set []Update 95 | // UPDATE IGNORE 96 | UseUpdateIgnore bool 97 | Insert Values 98 | InsertMultiple InsertMultiple 99 | // INSERT IGNORE INTO 100 | UseInsertIgnoreInto bool 101 | 102 | Where []Condition 103 | WhereOR [][]Condition 104 | WhereRaw Raw 105 | WhereAllowEmpty bool 106 | 107 | OrderBy []OrderBy 108 | OrderByRaw Raw 109 | 110 | GroupBy []Column 111 | GroupByRaw Raw 112 | 113 | Having []Condition 114 | HavingRaw Raw 115 | 116 | Limit uint64 117 | limitRaw limitRaw 118 | Offset uint64 119 | 120 | Lock SelectLock 121 | 122 | Join []Join 123 | Raw Raw 124 | 125 | Debug bool 126 | debugData struct{ id uint64 } 127 | PrintSQL bool 128 | Explain bool 129 | RunTime bool 130 | elapsedTimeData struct { 131 | startTime time.Time 132 | } 133 | LastQueryCost bool 134 | 135 | Review string 136 | Reviews []string 137 | SQLChecker SQLChecker 138 | disableSQLChecker bool 139 | } 140 | 141 | func (qb QB) mustInTransaction() error { 142 | if len(qb.Lock) == 0 { 143 | return nil 144 | } 145 | return xerr.New("goclub/sql: SELECT " + qb.Lock.String() + " must exec in transaction") 146 | } 147 | 148 | type FromRaw struct { 149 | TableName Raw 150 | SoftDeleteWhere Raw 151 | } 152 | type OrderBy struct { 153 | Column Column 154 | Type orderByType 155 | } 156 | type orderByType uint8 157 | 158 | const ( 159 | // 默认降序 160 | ASC orderByType = iota 161 | DESC 162 | ) 163 | 164 | type SelectLock string 165 | 166 | func (s SelectLock) String() string { 167 | return string(s) 168 | } 169 | 170 | const FORSHARE SelectLock = "FOR SHARE" 171 | const FORUPDATE SelectLock = "FOR UPDATE" 172 | 173 | type UnionTable struct { 174 | Tables []QB 175 | UnionAll bool 176 | } 177 | 178 | func (union UnionTable) SQLSelect() (raw Raw) { 179 | var sqlList stringQueue 180 | var subQueryList []string 181 | for _, table := range union.Tables { 182 | subQV := table.SQLSelect() 183 | subQueryList = append(subQueryList, "("+subQV.Query+")") 184 | raw.Values = append(raw.Values, subQV.Values...) 185 | } 186 | unionText := "UNION" 187 | if union.UnionAll { 188 | unionText += " ALL" 189 | } 190 | sqlList.Push(strings.Join(subQueryList, " "+unionText+" ")) 191 | raw.Query = sqlList.Join(" ") 192 | return 193 | } 194 | 195 | type limitRaw struct { 196 | Valid bool 197 | Limit uint64 198 | } 199 | type JoinType string 200 | 201 | func (t JoinType) String() string { 202 | return string(t) 203 | } 204 | 205 | const InnerJoin JoinType = "INNER JOIN" 206 | const LeftJoin JoinType = "LEFT JOIN" 207 | const RightJoin JoinType = "RIGHT JOIN" 208 | const FullOuterJoin JoinType = "FULL OUTER JOIN" 209 | const CrossJoin JoinType = "CROSS JOIN" 210 | 211 | type Join struct { 212 | Type JoinType 213 | TableName string 214 | On string 215 | } 216 | type Column string 217 | 218 | func (c Column) String() string { return string(c) } 219 | func (c Column) wrapField() string { 220 | s := c.String() 221 | return "`" + strings.ReplaceAll(s, ".", "`.`") + "`" 222 | } 223 | func (c Column) wrapFieldWithAS() string { 224 | s := c.String() 225 | column := c.wrapField() 226 | if strings.Contains(s, ".") { 227 | column += ` AS '` + s + `'` 228 | } 229 | return column 230 | } 231 | 232 | type Statement string 233 | 234 | const StatementSelect Statement = "SELECT" 235 | const StatementUpdate Statement = "UPDATE" 236 | const StatementDelete Statement = "DELETE" 237 | const StatementInsert Statement = "INSERT" 238 | 239 | func (s Statement) String() string { return string(s) } 240 | 241 | func (qb QB) SQL(statement Statement) Raw { 242 | originQB := qb 243 | if len(qb.Raw.Query) != 0 { 244 | return qb.Raw 245 | } 246 | if statement != StatementInsert && qb.whereIsEmpty() && qb.WhereAllowEmpty == false { 247 | cloneQB := originQB 248 | cloneQB.WhereAllowEmpty = true 249 | 250 | warning := "query:" + "\n" + 251 | "\t" + cloneQB.SQL(statement).Query + "\n" + 252 | "If you need where is empty, set qb.WhereAllowEmpty = true" 253 | Log.Warn("Maybe you forget qb.Where\n" + warning) 254 | } 255 | var values []interface{} 256 | var sqlList stringQueue 257 | if statement == StatementSelect && qb.UnionTable.Tables != nil { 258 | unionRaw := qb.UnionTable.SQLSelect() 259 | sqlList.Push(unionRaw.Query) 260 | values = append(values, unionRaw.Values...) 261 | } 262 | if qb.From != nil { 263 | qb.from = "`" + qb.From.TableName() + "`" 264 | switch statement { 265 | case StatementSelect, 266 | StatementUpdate: 267 | qb.softDelete = qb.From.SoftDeleteWhere() 268 | case StatementInsert: 269 | case StatementDelete: 270 | default: 271 | panic(xerr.New("statement can not be " + statement.String())) 272 | } 273 | } 274 | if qb.FromRaw.TableName.Query != "" { 275 | qb.from = qb.FromRaw.TableName.Query 276 | values = append(values, qb.FromRaw.TableName.Values...) 277 | qb.softDelete = qb.FromRaw.SoftDeleteWhere 278 | } 279 | switch statement { 280 | case StatementSelect: 281 | if qb.UnionTable.Tables == nil { 282 | sqlList.Push("SELECT") 283 | if qb.SelectRaw == nil { 284 | inputSelectLen := len(qb.Select) 285 | if qb.From != nil && inputSelectLen == 0 { 286 | qb.Select = TagToColumns(qb.From) 287 | } 288 | newSelectLen := len(qb.Select) 289 | if newSelectLen == 0 { 290 | warningTitle := "goclub/sql: (NO SELECT FIELD)" 291 | var warning string 292 | if qb.From != nil { 293 | warning = "qb.From field does not have db struct tag or you forget set qb.Select" 294 | } else { 295 | warning = "qb.Select is empty and qb.Form is nil, maybe you forget set qb.Select" 296 | } 297 | Log.Warn(warningTitle + "\n" + warning) 298 | return Raw{Query: warningTitle + " " + warning} 299 | } else { 300 | 301 | } 302 | sqlList.Push(strings.Join(columnsToStringsWithAS(qb.Select), ", ")) 303 | } else { 304 | var rawColumns []string 305 | for _, raws := range qb.SelectRaw { 306 | rawColumns = append(rawColumns, raws.Query) 307 | values = append(values, raws.Values...) 308 | } 309 | sqlList.Push(strings.Join(rawColumns, ", ")) 310 | } 311 | sqlList.Push("FROM") 312 | sqlList.Push(qb.from) 313 | } 314 | if qb.Index != "" { 315 | sqlList.Push(qb.Index) 316 | } 317 | for _, join := range qb.Join { 318 | sqlList.Push(join.Type.String()) 319 | sqlList.Push("`" + join.TableName + "`") 320 | sqlList.Push("ON") 321 | sqlList.Push(join.On) 322 | } 323 | case StatementUpdate: 324 | sqlList.Push("UPDATE") 325 | if qb.UseUpdateIgnore { 326 | sqlList.Push("IGNORE") 327 | } 328 | sqlList.Push(qb.from) 329 | sqlList.Push("SET") 330 | var sets []string 331 | for _, data := range qb.Set { 332 | if len(data.Raw.Query) != 0 { 333 | sets = append(sets, data.Raw.Query) 334 | values = append(values, data.Raw.Values...) 335 | } else { 336 | sets = append(sets, data.Column.wrapField()+" = ?") 337 | values = append(values, data.Value) 338 | } 339 | } 340 | sqlList.Push(strings.Join(sets, ", ")) 341 | case StatementDelete: 342 | sqlList.Push("DELETE FROM") 343 | sqlList.Push(qb.from) 344 | case StatementInsert: 345 | if qb.UseInsertIgnoreInto { 346 | sqlList.Push("INSERT IGNORE INTO") 347 | } else { 348 | sqlList.Push("INSERT INTO") 349 | } 350 | 351 | sqlList.Push(qb.from) 352 | if len(qb.Insert) != 0 { 353 | var insertValues []interface{} 354 | for _, insert := range qb.Insert { 355 | qb.InsertMultiple.Column = append(qb.InsertMultiple.Column, insert.Column) 356 | insertValues = append(insertValues, insert.Value) 357 | } 358 | qb.InsertMultiple.Values = append(qb.InsertMultiple.Values, insertValues) 359 | } 360 | 361 | var columns []string 362 | for _, column := range qb.InsertMultiple.Column { 363 | columns = append(columns, column.wrapField()) 364 | } 365 | var allPlaceholders []string 366 | for _, value := range qb.InsertMultiple.Values { 367 | var rowPlaceholder []string 368 | for _, v := range value { 369 | var insertRaw Raw 370 | var hasInsertRaw bool 371 | switch item := v.(type) { 372 | case sqlInsertRawer: 373 | insertRaw.Query, insertRaw.Values = item.SQLInsertRaw() 374 | hasInsertRaw = true 375 | } 376 | if hasInsertRaw { 377 | rowPlaceholder = append(rowPlaceholder, insertRaw.Query) 378 | values = append(values, insertRaw.Values...) 379 | } else { 380 | rowPlaceholder = append(rowPlaceholder, "?") 381 | values = append(values, v) 382 | } 383 | } 384 | allPlaceholders = append(allPlaceholders, "("+strings.Join(rowPlaceholder, ",")+")") 385 | 386 | } 387 | sqlList.Push("(" + strings.Join(columns, ",") + ")") 388 | sqlList.Push("VALUES") 389 | sqlList.Push(strings.Join(allPlaceholders, ",")) 390 | default: 391 | panic(xerr.New("incorrect statement")) 392 | } 393 | // where 394 | { 395 | var whereString string 396 | var whereRaw Raw 397 | if qb.WhereRaw.Query != "" { 398 | whereRaw = qb.WhereRaw 399 | } else { 400 | tooMuchWhere := len(qb.Where) != 0 && len(qb.WhereOR) != 0 401 | if tooMuchWhere { 402 | panic(xerr.New("if qb.WhereOR not empty, then qb.Where must be nil")) 403 | } 404 | if len(qb.Where) != 0 && len(qb.WhereOR) == 0 { 405 | qb.WhereOR = append(qb.WhereOR, qb.Where) 406 | } 407 | whereRaw = ConditionsSQL(qb.WhereOR) 408 | } 409 | var whereValues []interface{} 410 | whereString, whereValues = whereRaw.Query, whereRaw.Values 411 | values = append(values, whereValues...) 412 | var disableWhereIsEmpty bool 413 | if statement == StatementDelete || statement == StatementUpdate { 414 | disableWhereIsEmpty = true 415 | } 416 | if disableWhereIsEmpty && len(strings.TrimSpace(whereString)) == 0 { 417 | return Raw{"goclub/sql:(MAYBE_FORGET_WHERE)", nil} 418 | } 419 | if !qb.DisableSoftDelete { 420 | needSoftDelete := qb.softDelete.Query != "" 421 | if needSoftDelete { 422 | whereSoftDelete := qb.softDelete 423 | values = append(values, whereSoftDelete.Values...) 424 | if len(whereString) != 0 { 425 | whereString += " AND " + whereSoftDelete.Query 426 | } else { 427 | whereString += whereSoftDelete.Query 428 | } 429 | } 430 | } 431 | if len(whereString) != 0 { 432 | sqlList.Push("WHERE") 433 | sqlList.Push(whereString) 434 | } 435 | } 436 | // group by 437 | if qb.GroupByRaw.Query != "" { 438 | sqlList.Push("GROUP BY") 439 | sqlList.Push(qb.GroupByRaw.Query) 440 | values = append(values, qb.GroupByRaw.Values...) 441 | } else if len(qb.GroupBy) != 0 { 442 | sqlList.Push("GROUP BY") 443 | sqlList.Push(strings.Join(columnsToStrings(qb.GroupBy), ", ")) 444 | } 445 | // having 446 | if qb.HavingRaw.Query != "" { 447 | sqlList.Push("HAVING") 448 | sqlList.Push(qb.HavingRaw.Query) 449 | values = append(values, qb.HavingRaw.Values...) 450 | } else if len(qb.Having) != 0 { 451 | sqlList.Push("HAVING") 452 | havaingRaw := ConditionsSQL([][]Condition{qb.Having}) 453 | sqlList.Push(havaingRaw.Query) 454 | values = append(values, havaingRaw.Values...) 455 | } 456 | // order by 457 | if qb.OrderByRaw.Query != "" { 458 | sqlList.Push("ORDER BY") 459 | sqlList.Push(qb.OrderByRaw.Query) 460 | values = append(values, qb.OrderByRaw.Values...) 461 | } else if len(qb.OrderBy) != 0 { 462 | sqlList.Push("ORDER BY") 463 | var orderList stringQueue 464 | for _, order := range qb.OrderBy { 465 | switch order.Type { 466 | case ASC: 467 | orderList.Push(order.Column.wrapField() + " ASC") 468 | case DESC: 469 | orderList.Push(order.Column.wrapField() + " DESC") 470 | } 471 | } 472 | sqlList.Push(orderList.Join(", ")) 473 | } 474 | // limit 475 | limit := qb.Limit 476 | // 优先使用 qb.limitRaw, 因为 db.Count 需要用到 477 | if qb.limitRaw.Valid { 478 | limit = qb.limitRaw.Limit 479 | } 480 | if limit != 0 { 481 | sqlList.Push("LIMIT ?") 482 | values = append(values, qb.Limit) 483 | } 484 | // offset 485 | if qb.Offset != 0 { 486 | sqlList.Push("OFFSET ?") 487 | values = append(values, qb.Offset) 488 | } 489 | // lock 490 | if len(qb.Lock) != 0 { 491 | sqlList.Push(qb.Lock.String()) 492 | } 493 | query := sqlList.Join(" ") 494 | defer func() { 495 | if qb.Review != "" { 496 | qb.Reviews = append(qb.Reviews, qb.Review) 497 | } 498 | if len(qb.Reviews) != 0 && qb.disableSQLChecker == false { 499 | matched, refs, err := qb.SQLChecker.Check(qb.Reviews, query) 500 | if err != nil { 501 | qb.SQLChecker.TrackFail(qb.debugData.id, err, qb.Reviews, query, "") 502 | } else { 503 | if matched == false { 504 | qb.SQLChecker.TrackFail(qb.debugData.id, err, qb.Reviews, query, refs) 505 | } 506 | } 507 | } 508 | }() 509 | return Raw{query, values} 510 | } 511 | func (qb QB) SQLSelect() Raw { 512 | return qb.SQL(StatementSelect) 513 | } 514 | func (qb QB) SQLInsert() Raw { 515 | return qb.SQL(StatementInsert) 516 | } 517 | func (qb QB) SQLUpdate() Raw { 518 | return qb.SQL(StatementUpdate) 519 | } 520 | func (qb QB) SQLDelete() Raw { 521 | return qb.SQL(StatementDelete) 522 | } 523 | func (qb QB) Paging(page uint64, perPage uint32) QB { 524 | if page == 0 { 525 | page = 1 526 | } 527 | if perPage == 0 { 528 | perPage = 10 529 | } 530 | qb.Offset = (page - 1) * uint64(perPage) 531 | qb.Limit = uint64(perPage) 532 | return qb 533 | } 534 | func (qb *QB) execDebugBefore(ctx context.Context, storager Storager, statement Statement) { 535 | var err error 536 | defer func() { 537 | if err != nil { 538 | Log.Debug("error", "error", err) 539 | } 540 | }() 541 | debugID, err := rand.Int(rand.Reader, new(big.Int).SetUint64(9999)) 542 | if err != nil { 543 | // 这个错误故意不处理 544 | Log.Debug("error", "error", err) 545 | err = nil 546 | } 547 | qb.debugData.id = debugID.Uint64() 548 | var debugLog []string 549 | if qb.Debug { 550 | debugLog = append(debugLog, "Debug:") 551 | qb.PrintSQL = true 552 | qb.Explain = true 553 | qb.RunTime = true 554 | qb.LastQueryCost = true 555 | } 556 | qb.disableSQLChecker = true 557 | raw := qb.SQL(statement) 558 | qb.disableSQLChecker = false 559 | core := storager.getCore() 560 | // PrintSQL 561 | if qb.PrintSQL { 562 | debugLog = append(debugLog, renderSQL(qb.debugData.id, raw.Query, raw.Values)) 563 | } 564 | // EXPLAIN 565 | if qb.Explain { 566 | row := core.QueryRowxContext(ctx, "EXPLAIN "+raw.Query, raw.Values...) 567 | debugLog = append(debugLog, renderExplain(qb.debugData.id, row)) 568 | } 569 | if qb.RunTime { 570 | qb.elapsedTimeData.startTime = time.Now() 571 | } 572 | if len(debugLog) != 0 { 573 | Log.Debug(strings.Join(debugLog, "\n")) 574 | } 575 | return 576 | } 577 | 578 | func (qb *QB) execDebugAfter(ctx context.Context, storager Storager, statement Statement) { 579 | var err error 580 | defer func() { 581 | if err != nil { 582 | Log.Error("error", "error", err) 583 | } 584 | }() 585 | if qb.RunTime { 586 | Log.Debug(renderRunTime(qb.debugData.id, time.Now().Sub(qb.elapsedTimeData.startTime))) 587 | } 588 | if qb.LastQueryCost { 589 | var lastQueryCost float64 590 | lastQueryCost, err = coreLastQueryCost(ctx, storager) 591 | if err != nil { 592 | return 593 | } 594 | Log.Debug(renderLastQueryCost(qb.debugData.id, lastQueryCost)) 595 | } 596 | } 597 | 598 | func (qb QB) whereIsEmpty() bool { 599 | return qb.Raw.IsZero() && len(qb.Where) == 0 && len(qb.WhereOR) == 0 && qb.WhereRaw.IsZero() 600 | } 601 | -------------------------------------------------------------------------------- /query_builder_test.go: -------------------------------------------------------------------------------- 1 | package sq_test 2 | 3 | import ( 4 | sq "github.com/goclub/sql" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/stretchr/testify/suite" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestQB(t *testing.T) { 12 | suite.Run(t, new(TestQBSuite)) 13 | } 14 | 15 | type TestQBSuite struct { 16 | suite.Suite 17 | } 18 | 19 | func (suite TestQBSuite) TestTable() { 20 | t := suite.T() 21 | qb := sq.QB{ 22 | From: &User{}, 23 | WhereAllowEmpty: true, 24 | } 25 | raw := qb.SQLSelect() 26 | query, values := raw.Query, raw.Values 27 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL", query) 28 | assert.Equal(t, []interface{}(nil), values) 29 | } 30 | func (suite TestQBSuite) TestTableRaw() { 31 | t := suite.T() 32 | { 33 | qb := sq.QB{ 34 | SelectRaw: []sq.Raw{{Query: "*"}}, 35 | WhereAllowEmpty: true, 36 | FromRaw: sq.FromRaw{ 37 | TableName: sq.Raw{"(SELECT * FROM `user` WHERE `name` like ?) as user", []interface{}{"%tableRaw%"}}, 38 | SoftDeleteWhere: sq.Raw{}, 39 | }, 40 | } 41 | raw := qb.SQLSelect() 42 | query, values := raw.Query, raw.Values 43 | assert.Equal(t, "SELECT * FROM (SELECT * FROM `user` WHERE `name` like ?) as user", query) 44 | assert.Equal(t, []interface{}{"%tableRaw%"}, values) 45 | } 46 | 47 | } 48 | func (suite TestQBSuite) TestDisableSoftDelete() { 49 | t := suite.T() 50 | { 51 | qb := sq.QB{ 52 | From: &User{}, 53 | DisableSoftDelete: true, 54 | } 55 | raw := qb.SQLSelect() 56 | query, values := raw.Query, raw.Values 57 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user`", query) 58 | assert.Equal(t, []interface{}(nil), values) 59 | } 60 | { 61 | qb := sq.QB{ 62 | From: &User{}, 63 | WhereAllowEmpty: true, 64 | DisableSoftDelete: false, 65 | } 66 | raw := qb.SQLSelect() 67 | query, values := raw.Query, raw.Values 68 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL", query) 69 | assert.Equal(t, []interface{}(nil), values) 70 | } 71 | } 72 | func (suite TestQBSuite) TestUnionTable() { 73 | t := suite.T() 74 | { 75 | where := sq.And("age", sq.GT(18)) 76 | qb := sq.QB{ 77 | UnionTable: sq.UnionTable{ 78 | Tables: []sq.QB{ 79 | { 80 | From: &User{}, 81 | Where: where, 82 | }, 83 | { 84 | From: &User{}, 85 | Where: where, 86 | }, 87 | }, 88 | UnionAll: true, 89 | }, 90 | Where: []sq.Condition{ 91 | {"id", sq.Equal(1)}, 92 | }, 93 | } 94 | raw := qb.SQLSelect() 95 | query, values := raw.Query, raw.Values 96 | assert.Equal(t, "(SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `deleted_at` IS NULL) UNION ALL (SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `deleted_at` IS NULL) WHERE `id` = ?", query) 97 | assert.Equal(t, []interface{}{18, 18, 1}, values) 98 | } 99 | { 100 | where := sq.And("age", sq.GT(18)) 101 | qb := sq.QB{ 102 | UnionTable: sq.UnionTable{ 103 | Tables: []sq.QB{ 104 | { 105 | From: &User{}, 106 | Where: where, 107 | }, 108 | { 109 | From: &User{}, 110 | Where: where, 111 | }, 112 | }, 113 | UnionAll: false, 114 | }, 115 | Where: []sq.Condition{ 116 | {"id", sq.Equal(1)}, 117 | }, 118 | } 119 | raw := qb.SQLSelect() 120 | query, values := raw.Query, raw.Values 121 | assert.Equal(t, "(SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `deleted_at` IS NULL) UNION (SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `deleted_at` IS NULL) WHERE `id` = ?", query) 122 | assert.Equal(t, []interface{}{18, 18, 1}, values) 123 | } 124 | } 125 | func (suite TestQBSuite) TestSelect() { 126 | t := suite.T() 127 | { 128 | qb := sq.QB{ 129 | FromRaw: sq.FromRaw{ 130 | TableName: sq.Raw{"user", nil}, 131 | SoftDeleteWhere: sq.Raw{}, 132 | }, 133 | SelectRaw: []sq.Raw{{Query: "*"}}, 134 | Where: []sq.Condition{ 135 | {"id", sq.Equal(1)}, 136 | }, 137 | } 138 | raw := qb.SQLSelect() 139 | query, values := raw.Query, raw.Values 140 | assert.Equal(t, "SELECT * FROM user WHERE `id` = ?", query) 141 | assert.Equal(t, []interface{}{1}, values) 142 | } 143 | { 144 | qb := sq.QB{ 145 | FromRaw: sq.FromRaw{ 146 | TableName: sq.Raw{"user", nil}, 147 | SoftDeleteWhere: sq.Raw{}, 148 | }, 149 | Select: nil, 150 | Where: []sq.Condition{ 151 | {"id", sq.Equal(1)}, 152 | }, 153 | } 154 | raw := qb.SQLSelect() 155 | query, values := raw.Query, raw.Values 156 | assert.Equal(t, "goclub/sql: (NO SELECT FIELD) qb.Select is empty and qb.Form is nil, maybe you forget set qb.Select", query) 157 | assert.Nil(t, values) 158 | } 159 | { 160 | qb := sq.QB{ 161 | FromRaw: sq.FromRaw{ 162 | TableName: sq.Raw{"user", nil}, 163 | SoftDeleteWhere: sq.Raw{}, 164 | }, 165 | Select: []sq.Column{"name"}, 166 | Where: []sq.Condition{ 167 | {"id", sq.Equal(1)}, 168 | }, 169 | } 170 | raw := qb.SQLSelect() 171 | query, values := raw.Query, raw.Values 172 | assert.Equal(t, "SELECT `name` FROM user WHERE `id` = ?", query) 173 | assert.Equal(t, []interface{}{1}, values) 174 | } 175 | } 176 | func (suite TestQBSuite) TestSelectRaw() { 177 | t := suite.T() 178 | qb := sq.QB{ 179 | From: &User{}, 180 | // Select 会被忽略 优先使用 SelectRaw 181 | Select: []sq.Column{"name"}, 182 | SelectRaw: []sq.Raw{ 183 | sq.Raw{"count(*) as count", nil}, 184 | }, 185 | Where: []sq.Condition{ 186 | {"id", sq.Equal(1)}, 187 | }, 188 | } 189 | raw := qb.SQLSelect() 190 | query, values := raw.Query, raw.Values 191 | assert.Equal(t, "SELECT count(*) as count FROM `user` WHERE `id` = ? AND `deleted_at` IS NULL", query) 192 | assert.Equal(t, []interface{}{1}, values) 193 | } 194 | func (suite TestQBSuite) TestSelectColumnHasDot() { 195 | t := suite.T() 196 | qb := sq.QB{ 197 | FromRaw: sq.FromRaw{ 198 | TableName: sq.Raw{"user as u", nil}, 199 | SoftDeleteWhere: sq.Raw{}, 200 | }, 201 | Select: []sq.Column{`u.name`}, 202 | Where: []sq.Condition{ 203 | {"id", sq.Equal(1)}, 204 | }, 205 | } 206 | raw := qb.SQLSelect() 207 | query, values := raw.Query, raw.Values 208 | assert.Equal(t, "SELECT `u`.`name` AS 'u.name' FROM user as u WHERE `id` = ?", query) 209 | assert.Equal(t, []interface{}{1}, values) 210 | } 211 | func (suite TestQBSuite) TestIndex() { 212 | t := suite.T() 213 | qb := sq.QB{ 214 | From: &User{}, 215 | Index: "USE INDEX(PRIMARY)", 216 | WhereAllowEmpty: true, 217 | } 218 | raw := qb.SQLSelect() 219 | query, values := raw.Query, raw.Values 220 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` USE INDEX(PRIMARY) WHERE `deleted_at` IS NULL", query) 221 | assert.Equal(t, []interface{}(nil), values) 222 | } 223 | 224 | func (suite TestQBSuite) TestWhere() { 225 | t := suite.T() 226 | { 227 | qb := sq.QB{ 228 | From: &User{}, 229 | Where: []sq.Condition{ 230 | {"name", sq.Equal("nimo")}, 231 | }, 232 | } 233 | raw := qb.SQLSelect() 234 | query, values := raw.Query, raw.Values 235 | assert.Equal(t, sq.ToConditions(qb.Where), sq.And("name", sq.Equal("nimo"))) 236 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` = ? AND `deleted_at` IS NULL", query) 237 | assert.Equal(t, []interface{}{"nimo"}, values) 238 | } 239 | } 240 | 241 | func (suite TestQBSuite) TestWhereOR() { 242 | t := suite.T() 243 | { 244 | qb := sq.QB{ 245 | From: &User{}, 246 | WhereOR: [][]sq.Condition{ 247 | {{"name", sq.Equal("nimo")}}, 248 | {{"name", sq.Equal("nico")}}, 249 | }, 250 | } 251 | raw := qb.SQLSelect() 252 | query, values := raw.Query, raw.Values 253 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE (`name` = ?) OR (`name` = ?) AND `deleted_at` IS NULL", query) 254 | assert.Equal(t, []interface{}{"nimo", "nico"}, values) 255 | } 256 | { 257 | qb := sq.QB{ 258 | From: &User{}, 259 | Select: []sq.Column{"id"}, 260 | // WHERE (`name` LIKE ? OR `mobile` LIKE ?) AND `role_id` = ? 261 | Where: sq. 262 | OrGroup( 263 | sq.Condition{"name", sq.Like("nimo")}, 264 | sq.Condition{"mobile", sq.Like("13611112222")}, 265 | ). 266 | And("role_id", sq.Equal("1")), 267 | } 268 | raw := qb.SQLSelect() 269 | query, values := raw.Query, raw.Values 270 | ands := []sq.Condition{ 271 | { 272 | "", sq.OP{ 273 | OrGroup: []sq.Condition{ 274 | {"name", sq.Like("nimo")}, 275 | {"mobile", sq.Like("13611112222")}, 276 | }, 277 | }, 278 | }, 279 | {"role_id", sq.Equal("1")}, 280 | } 281 | assert.Equal(t, ands, qb.Where) 282 | assert.Equal(t, "SELECT `id` FROM `user` WHERE (`name` LIKE ? OR `mobile` LIKE ?) AND `role_id` = ? AND `deleted_at` IS NULL", query) 283 | assert.Equal(t, []interface{}{"%nimo%", "%13611112222%", "1"}, values) 284 | } 285 | { 286 | qb := sq.QB{ 287 | From: &User{}, 288 | Select: []sq.Column{"id"}, 289 | // WHERE (`name` LIKE ? OR `mobile` LIKE ?) AND `role_id` = ? 290 | Where: sq. 291 | OrGroup( 292 | sq.Condition{"name", sq.IF(true, sq.Like("nimo"))}, 293 | sq.Condition{"mobile", sq.IF(false, sq.Like("13611112222"))}, 294 | ). 295 | And("role_id", sq.Equal("1")), 296 | } 297 | raw := qb.SQLSelect() 298 | query, values := raw.Query, raw.Values 299 | assert.Equal(t, "SELECT `id` FROM `user` WHERE (`name` LIKE ?) AND `role_id` = ? AND `deleted_at` IS NULL", query) 300 | assert.Equal(t, []interface{}{"%nimo%", "1"}, values) 301 | } 302 | { 303 | qb := sq.QB{ 304 | From: &User{}, 305 | Select: []sq.Column{"id"}, 306 | // WHERE (`name` LIKE ? OR `mobile` LIKE ?) AND `role_id` = ? 307 | Where: sq. 308 | OrGroup( 309 | sq.Condition{"name", sq.IF(false, sq.Like("nimo"))}, 310 | sq.Condition{"mobile", sq.IF(true, sq.Like("13611112222"))}, 311 | ). 312 | And("role_id", sq.Equal("1")), 313 | } 314 | raw := qb.SQLSelect() 315 | query, values := raw.Query, raw.Values 316 | assert.Equal(t, "SELECT `id` FROM `user` WHERE (`mobile` LIKE ?) AND `role_id` = ? AND `deleted_at` IS NULL", query) 317 | assert.Equal(t, []interface{}{"%13611112222%", "1"}, values) 318 | } 319 | { 320 | qb := sq.QB{ 321 | From: &User{}, 322 | Select: []sq.Column{"id"}, 323 | // WHERE (`name` LIKE ? OR `mobile` LIKE ?) AND `role_id` = ? 324 | Where: sq. 325 | OrGroup( 326 | sq.Condition{"name", sq.IF(false, sq.Like("nimo"))}, 327 | sq.Condition{"mobile", sq.IF(false, sq.Like("13611112222"))}, 328 | ). 329 | And("role_id", sq.Equal("1")), 330 | } 331 | raw := qb.SQLSelect() 332 | query, values := raw.Query, raw.Values 333 | assert.Equal(t, "SELECT `id` FROM `user` WHERE `role_id` = ? AND `deleted_at` IS NULL", query) 334 | assert.Equal(t, []interface{}{"1"}, values) 335 | } 336 | } 337 | 338 | func (suite TestQBSuite) TestWhereRaw() { 339 | t := suite.T() 340 | { 341 | qb := sq.QB{ 342 | From: &User{}, 343 | WhereRaw: sq.Raw{"`name` = ? AND `age` = ?", []interface{}{"nimo", 1}}, 344 | } 345 | raw := qb.SQLSelect() 346 | query, values := raw.Query, raw.Values 347 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` = ? AND `age` = ? AND `deleted_at` IS NULL", query) 348 | assert.Equal(t, []interface{}{"nimo", 1}, values) 349 | } 350 | { 351 | } 352 | } 353 | func (suite TestQBSuite) TestWhereOPRaw() { 354 | t := suite.T() 355 | { 356 | qb := sq.QB{ 357 | From: &User{}, 358 | Where: []sq.Condition{ 359 | sq.ConditionRaw("`name` = `cname`", nil), 360 | sq.ConditionRaw("`age` = ?", []interface{}{1}), 361 | }, 362 | } 363 | raw := qb.SQLSelect() 364 | query, values := raw.Query, raw.Values 365 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` = `cname` AND `age` = ? AND `deleted_at` IS NULL", query) 366 | assert.Equal(t, []interface{}{1}, values) 367 | } 368 | { 369 | qb := sq.QB{ 370 | From: &User{}, 371 | Where: []sq.Condition{ 372 | sq.ConditionRaw("`name` = ?", []interface{}{"nimo"}), 373 | }, 374 | } 375 | raw := qb.SQLSelect() 376 | query, values := raw.Query, raw.Values 377 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` = ? AND `deleted_at` IS NULL", query) 378 | assert.Equal(t, []interface{}{"nimo"}, values) 379 | } 380 | } 381 | func (suite TestQBSuite) TestWhereSubQuery() { 382 | t := suite.T() 383 | { 384 | qb := sq.QB{ 385 | From: &User{}, 386 | Where: []sq.Condition{ 387 | {"id", sq.SubQuery("IN", sq.QB{ 388 | From: &User{}, 389 | WhereAllowEmpty: true, 390 | Select: []sq.Column{"id"}, 391 | })}, 392 | }, 393 | } 394 | raw := qb.SQLSelect() 395 | query, values := raw.Query, raw.Values 396 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `id` IN (SELECT `id` FROM `user` WHERE `deleted_at` IS NULL) AND `deleted_at` IS NULL", query) 397 | assert.Equal(t, []interface{}(nil), values) 398 | } 399 | } 400 | func (suite TestQBSuite) TestWhereAndTwoCondition() { 401 | t := suite.T() 402 | { 403 | qb := sq.QB{ 404 | From: &User{}, 405 | Where: []sq.Condition{ 406 | {"name", sq.Equal("nimo")}, 407 | {"age", sq.Equal(18)}, 408 | }, 409 | } 410 | raw := qb.SQLSelect() 411 | query, values := raw.Query, raw.Values 412 | ands := []sq.Condition( 413 | sq.And("name", sq.Equal("nimo")).And("age", sq.Equal(18)), 414 | ) 415 | assert.Equal(t, qb.Where, ands) 416 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` = ? AND `age` = ? AND `deleted_at` IS NULL", query) 417 | assert.Equal(t, []interface{}{"nimo", 18}, values) 418 | } 419 | } 420 | func (suite TestQBSuite) TestWhereOPGTLTint() { 421 | t := suite.T() 422 | { 423 | qb := sq.QB{ 424 | From: &User{}, 425 | Where: []sq.Condition{ 426 | {"age", sq.GT(18)}, 427 | {"age", sq.LT(19)}, 428 | }, 429 | } 430 | raw := qb.SQLSelect() 431 | query, values := raw.Query, raw.Values 432 | ands := []sq.Condition( 433 | sq.And("age", sq.GT(18)).And("age", sq.LT(19)), 434 | ) 435 | assert.Equal(t, qb.Where, ands) 436 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `age` < ? AND `deleted_at` IS NULL", query) 437 | assert.Equal(t, []interface{}{18, 19}, values) 438 | } 439 | { 440 | qb := sq.QB{ 441 | From: &User{}, 442 | Where: []sq.Condition{ 443 | {"age", sq.GTE(18)}, 444 | {"age", sq.LTE(19)}, 445 | }, 446 | } 447 | raw := qb.SQLSelect() 448 | query, values := raw.Query, raw.Values 449 | ands := []sq.Condition( 450 | sq.And("age", sq.GTE(18)).And("age", sq.LTE(19)), 451 | ) 452 | assert.Equal(t, qb.Where, ands) 453 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` >= ? AND `age` <= ? AND `deleted_at` IS NULL", query) 454 | assert.Equal(t, []interface{}{18, 19}, values) 455 | } 456 | } 457 | func (suite TestQBSuite) TestWhereOPGTLTflaot() { 458 | t := suite.T() 459 | { 460 | qb := sq.QB{ 461 | From: &User{}, 462 | Where: []sq.Condition{ 463 | {"age", sq.GT(18.11)}, 464 | {"age", sq.LT(19.22)}, 465 | }, 466 | } 467 | raw := qb.SQLSelect() 468 | query, values := raw.Query, raw.Values 469 | ands := []sq.Condition( 470 | sq.And("age", sq.GT(18.11)).And("age", sq.LT(19.22)), 471 | ) 472 | assert.Equal(t, qb.Where, ands) 473 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `age` < ? AND `deleted_at` IS NULL", query) 474 | assert.Equal(t, []interface{}{18.11, 19.22}, values) 475 | } 476 | { 477 | qb := sq.QB{ 478 | From: &User{}, 479 | Where: []sq.Condition{ 480 | {"age", sq.GTE(18.11)}, 481 | {"age", sq.LTE(19.22)}, 482 | }, 483 | } 484 | raw := qb.SQLSelect() 485 | query, values := raw.Query, raw.Values 486 | ands := []sq.Condition( 487 | sq.And("age", sq.GTE(18.11)).And("age", sq.LTE(19.22)), 488 | ) 489 | assert.Equal(t, qb.Where, ands) 490 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` >= ? AND `age` <= ? AND `deleted_at` IS NULL", query) 491 | assert.Equal(t, []interface{}{18.11, 19.22}, values) 492 | } 493 | } 494 | 495 | // func (suite TestQBSuite) TestWhereOPGtTimeLtTime() { 496 | // t := suite.T() 497 | // startTime := time.Date(2020,11,11,22,22,22,0, time.UTC) 498 | // endTime := time.Date(2020,11,11,22,22,22,0, time.UTC) 499 | // 500 | // { 501 | // qb := sq.QB{ 502 | // From: &User{}, 503 | // Where: []sq.Condition{ 504 | // {"age", sq.GtTime(startTime)}, 505 | // {"age", sq.LtTime(endTime)}, 506 | // }, 507 | // } 508 | // raw := qb.SQLSelect(); query, values := raw.Query, raw.Values 509 | // ands := []sq.Condition( 510 | // sq.And("age", sq.GtTime(startTime)).And("age", sq.LtTime(endTime)), 511 | // ) 512 | // assert.Equal(t, qb.Where, ands) 513 | // assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` > ? AND `age` < ? AND `deleted_at` IS NULL", query) 514 | // assert.Equal(t, []interface{}{startTime, endTime}, values) 515 | // } 516 | // { 517 | // qb := sq.QB{ 518 | // From: &User{}, 519 | // Where: []sq.Condition{ 520 | // {"age", sq.GtOrEqualTime(startTime)}, 521 | // {"age", sq.LtOrEqualTime(endTime)}, 522 | // }, 523 | // } 524 | // raw := qb.SQLSelect(); query, values := raw.Query, raw.Values 525 | // ands := []sq.Condition( 526 | // sq.And("age", sq.GtOrEqualTime(startTime)).And("age", sq.LtOrEqualTime(endTime)), 527 | // ) 528 | // assert.Equal(t, qb.Where, ands) 529 | // assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` >= ? AND `age` <= ? AND `deleted_at` IS NULL", query) 530 | // assert.Equal(t, []interface{}{startTime, endTime}, values) 531 | // } 532 | // } 533 | 534 | func (suite TestQBSuite) TestWhereEqualAndNotEqual() { 535 | t := suite.T() 536 | qb := sq.QB{ 537 | From: &User{}, 538 | Where: []sq.Condition{ 539 | {"name", sq.Equal("nimo")}, 540 | {"book", sq.NotEqual("abc")}, 541 | }, 542 | } 543 | raw := qb.SQLSelect() 544 | query, values := raw.Query, raw.Values 545 | ands := []sq.Condition( 546 | sq.And("name", sq.Equal("nimo")).And("book", sq.NotEqual("abc")), 547 | ) 548 | assert.Equal(t, qb.Where, ands) 549 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` = ? AND `book` <> ? AND `deleted_at` IS NULL", query) 550 | assert.Equal(t, []interface{}{"nimo", "abc"}, values) 551 | } 552 | 553 | func (suite TestQBSuite) TestWhereLike() { 554 | t := suite.T() 555 | qb := sq.QB{ 556 | From: &User{}, 557 | Where: []sq.Condition{ 558 | {"name", sq.Like("nimo")}, 559 | }, 560 | } 561 | raw := qb.SQLSelect() 562 | query, values := raw.Query, raw.Values 563 | ands := []sq.Condition( 564 | sq.And("name", sq.Like("nimo")), 565 | ) 566 | assert.Equal(t, qb.Where, ands) 567 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` LIKE ? AND `deleted_at` IS NULL", query) 568 | assert.Equal(t, []interface{}{"%nimo%"}, values) 569 | } 570 | 571 | func (suite TestQBSuite) TestWhereLikeLeft() { 572 | t := suite.T() 573 | qb := sq.QB{ 574 | From: &User{}, 575 | Where: []sq.Condition{ 576 | {"name", sq.LikeLeft("nimo")}, 577 | }, 578 | } 579 | raw := qb.SQLSelect() 580 | query, values := raw.Query, raw.Values 581 | ands := []sq.Condition( 582 | sq.And("name", sq.LikeLeft("nimo")), 583 | ) 584 | assert.Equal(t, qb.Where, ands) 585 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` LIKE ? AND `deleted_at` IS NULL", query) 586 | assert.Equal(t, []interface{}{"nimo%"}, values) 587 | } 588 | func (suite TestQBSuite) TestWhereLikeRight() { 589 | t := suite.T() 590 | qb := sq.QB{ 591 | From: &User{}, 592 | Where: []sq.Condition{ 593 | {"name", sq.LikeRight("nimo")}, 594 | }, 595 | } 596 | raw := qb.SQLSelect() 597 | query, values := raw.Query, raw.Values 598 | ands := []sq.Condition( 599 | sq.And("name", sq.LikeRight("nimo")), 600 | ) 601 | assert.Equal(t, qb.Where, ands) 602 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `name` LIKE ? AND `deleted_at` IS NULL", query) 603 | assert.Equal(t, []interface{}{"%nimo"}, values) 604 | } 605 | 606 | func (suite TestQBSuite) TestWhereIn() { 607 | t := suite.T() 608 | qb := sq.QB{ 609 | From: &User{}, 610 | Where: []sq.Condition{ 611 | {"id", sq.In([]string{"a", "b"})}, 612 | }, 613 | } 614 | raw := qb.SQLSelect() 615 | query, values := raw.Query, raw.Values 616 | ands := []sq.Condition( 617 | sq.And("id", sq.In([]string{"a", "b"})), 618 | ) 619 | assert.Equal(t, qb.Where, ands) 620 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `id` IN (?,?) AND `deleted_at` IS NULL", query) 621 | assert.Equal(t, []interface{}{"a", "b"}, values) 622 | } 623 | func (suite TestQBSuite) TestWhereIgnore() { 624 | t := suite.T() 625 | test := func(searchName string, query string, values []interface{}) { 626 | qb := sq.QB{ 627 | From: &User{}, 628 | Select: []sq.Column{"id"}, 629 | Where: sq.And("name", sq.IF(searchName != "", sq.Equal(searchName))), 630 | Reviews: []string{ 631 | "SELECT `id` FROM `user` WHERE `name` = ? AND `deleted_at` IS NULL", 632 | "SELECT `id` FROM `user` WHERE `deleted_at` IS NULL", 633 | }, 634 | SQLChecker: sq.DefaultSQLChecker{}, 635 | } 636 | raw := qb.SQLSelect() 637 | assert.Equal(t, query, raw.Query) 638 | assert.Equal(t, values, raw.Values) 639 | } 640 | test("", "SELECT `id` FROM `user` WHERE `deleted_at` IS NULL", []interface{}(nil)) 641 | test("nimo", "SELECT `id` FROM `user` WHERE `name` = ? AND `deleted_at` IS NULL", []interface{}{"nimo"}) 642 | { 643 | raw := sq.QB{ 644 | From: &User{}, 645 | Where: sq.And("name", sq.IF(false, sq.Equal("nimo"))), 646 | DisableSoftDelete: true, 647 | }.SQLSelect() 648 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user`", raw.Query) 649 | assert.Equal(t, []interface{}(nil), raw.Values) 650 | } 651 | { 652 | raw := sq.QB{ 653 | From: &User{}, 654 | Where: sq.And("name", sq.IF(false, sq.Equal("nimo"))).And("age", sq.IF(false, sq.Equal(1))), 655 | DisableSoftDelete: true, 656 | }.SQLSelect() 657 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user`", raw.Query) 658 | assert.Equal(t, []interface{}(nil), raw.Values) 659 | } 660 | 661 | } 662 | func (suite TestQBSuite) TestInPanic() { 663 | t := suite.T() 664 | var panicValue interface{} 665 | func() { 666 | defer func() { 667 | panicValue = recover() 668 | }() 669 | qb := sq.QB{ 670 | From: &User{}, 671 | Where: []sq.Condition{ 672 | {"id", sq.In("a")}, 673 | }, 674 | } 675 | _ = qb.SQLSelect() 676 | }() 677 | 678 | assert.Equal(t, panicValue.(error).Error(), "sq.In(string) slice must be slice") 679 | } 680 | 681 | func (suite TestQBSuite) TestLimit() { 682 | t := suite.T() 683 | { 684 | qb := sq.QB{ 685 | From: &User{}, 686 | Limit: 1, 687 | WhereAllowEmpty: true, 688 | } 689 | raw := qb.SQLSelect() 690 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL LIMIT ?") 691 | assert.Equal(t, []interface{}{uint64(1)}, raw.Values) 692 | } 693 | { 694 | qb := sq.QB{ 695 | From: &User{}, 696 | WhereAllowEmpty: true, 697 | } 698 | raw := qb.SQLSelect() 699 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL") 700 | assert.Equal(t, []interface{}(nil), raw.Values) 701 | } 702 | } 703 | func (suite TestQBSuite) TestOffset() { 704 | t := suite.T() 705 | { 706 | qb := sq.QB{ 707 | From: &User{}, 708 | WhereAllowEmpty: true, 709 | Offset: 100, 710 | } 711 | raw := qb.SQLSelect() 712 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL OFFSET ?") 713 | assert.Equal(t, []interface{}{uint64(100)}, raw.Values) 714 | } 715 | { 716 | qb := sq.QB{ 717 | From: &User{}, 718 | WhereAllowEmpty: true, 719 | } 720 | raw := qb.SQLSelect() 721 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL") 722 | assert.Equal(t, []interface{}(nil), raw.Values) 723 | } 724 | } 725 | func (suite TestQBSuite) TestLimitOffset() { 726 | t := suite.T() 727 | { 728 | qb := sq.QB{ 729 | From: &User{}, 730 | WhereAllowEmpty: true, 731 | Limit: 2, 732 | Offset: 100, 733 | } 734 | raw := qb.SQLSelect() 735 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL LIMIT ? OFFSET ?") 736 | assert.Equal(t, []interface{}{uint64(2), uint64(100)}, raw.Values) 737 | } 738 | } 739 | func (suite TestQBSuite) TestLock() { 740 | t := suite.T() 741 | { 742 | qb := sq.QB{ 743 | From: &User{}, 744 | Lock: sq.FORSHARE, 745 | WhereAllowEmpty: true, 746 | Limit: 1, 747 | } 748 | raw := qb.SQLSelect() 749 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL LIMIT ? FOR SHARE") 750 | assert.Equal(t, []interface{}{uint64(1)}, raw.Values) 751 | } 752 | { 753 | qb := sq.QB{ 754 | From: &User{}, 755 | WhereAllowEmpty: true, 756 | Lock: sq.FORUPDATE, 757 | } 758 | raw := qb.SQLSelect() 759 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL FOR UPDATE") 760 | assert.Equal(t, []interface{}(nil), raw.Values) 761 | } 762 | } 763 | func (suite TestQBSuite) TestJoin() { 764 | t := suite.T() 765 | uaCol := UserWithAddress{}.Column() 766 | qb := sq.QB{ 767 | FromRaw: sq.FromRaw{ 768 | TableName: sq.Raw{"user", nil}, 769 | SoftDeleteWhere: sq.Raw{"`user`.`deleted_at` is NULL AND user_address`.`deleted_at` is NULL", nil}, 770 | }, 771 | Select: []sq.Column{"user.id", "user_address.address"}, 772 | Where: sq.And(uaCol.UserID, sq.Equal(1)), 773 | Join: []sq.Join{ 774 | { 775 | Type: sq.LeftJoin, 776 | TableName: "`user_address`", 777 | On: "`user`.`id` = `user_address`.`user_id`", 778 | }, 779 | }, 780 | } 781 | raw := qb.SQLSelect() 782 | assert.Equal(t, raw.Query, "SELECT `user`.`id` AS 'user.id', `user_address`.`address` AS 'user_address.address' FROM user LEFT JOIN ``user_address`` ON `user`.`id` = `user_address`.`user_id` WHERE `user`.`id` = ? AND `user`.`deleted_at` is NULL AND user_address`.`deleted_at` is NULL") 783 | assert.Equal(t, []interface{}{1}, raw.Values) 784 | } 785 | func (suite TestQBSuite) TestJoinType() { 786 | t := suite.T() 787 | assert.Equal(t, sq.LeftJoin.String(), "LEFT JOIN") 788 | assert.Equal(t, sq.LeftJoin.String(), string(sq.LeftJoin)) 789 | } 790 | func (suite TestQBSuite) TestStatement() { 791 | t := suite.T() 792 | assert.Equal(t, sq.StatementSelect.String(), "SELECT") 793 | assert.Equal(t, sq.StatementSelect.String(), string(sq.StatementSelect)) 794 | } 795 | 796 | func (suite TestQBSuite) TestOrderBy() { 797 | t := suite.T() 798 | { 799 | qb := sq.QB{ 800 | From: &User{}, 801 | WhereAllowEmpty: true, 802 | Limit: 2, 803 | Offset: 10, 804 | OrderBy: []sq.OrderBy{{Column: "name"}}, 805 | } 806 | raw := qb.SQLSelect() 807 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL ORDER BY `name` ASC LIMIT ? OFFSET ?") 808 | assert.Equal(t, []interface{}{uint64(2), uint64(10)}, raw.Values) 809 | } 810 | { 811 | qb := sq.QB{ 812 | From: &User{}, 813 | WhereAllowEmpty: true, 814 | Limit: 2, 815 | Offset: 10, 816 | OrderBy: []sq.OrderBy{{"name", sq.DESC}}, 817 | } 818 | raw := qb.SQLSelect() 819 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL ORDER BY `name` DESC LIMIT ? OFFSET ?") 820 | assert.Equal(t, []interface{}{uint64(2), uint64(10)}, raw.Values) 821 | } 822 | { 823 | qb := sq.QB{ 824 | From: &User{}, 825 | Limit: 2, 826 | Offset: 10, 827 | OrderBy: []sq.OrderBy{{"name", sq.DESC}, {"age", sq.ASC}}, 828 | } 829 | raw := qb.SQLSelect() 830 | assert.Equal(t, raw.Query, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL ORDER BY `name` DESC, `age` ASC LIMIT ? OFFSET ?") 831 | assert.Equal(t, []interface{}{uint64(2), uint64(10)}, raw.Values) 832 | } 833 | } 834 | func (suite TestQBSuite) TestUnsafeDelete() { 835 | t := suite.T() 836 | qb := sq.QB{ 837 | From: &User{}, 838 | WhereAllowEmpty: true, 839 | } 840 | raw := qb.SQLDelete() 841 | assert.Equal(t, "goclub/sql:(MAYBE_FORGET_WHERE)", raw.Query) 842 | assert.Equal(t, []interface{}(nil), raw.Values) 843 | } 844 | func (suite TestQBSuite) TestHaving() { 845 | t := suite.T() 846 | qb := sq.QB{ 847 | From: &User{}, 848 | WhereAllowEmpty: true, 849 | SelectRaw: []sq.Raw{ 850 | {"`name`", nil}, 851 | {"count(*) AS count", nil}, 852 | }, 853 | GroupBy: []sq.Column{"name"}, 854 | Having: sq.And("count", sq.GT(1)), 855 | } 856 | raw := qb.SQLSelect() 857 | assert.Equal(t, "SELECT `name`, count(*) AS count FROM `user` WHERE `deleted_at` IS NULL GROUP BY `name` HAVING `count` > ?", raw.Query) 858 | assert.Equal(t, []interface{}{1}, raw.Values) 859 | } 860 | func (suite TestQBSuite) TestInsert() { 861 | t := suite.T() 862 | qb := sq.QB{ 863 | From: &User{}, 864 | UseInsertIgnoreInto: true, 865 | Insert: []sq.Insert{ 866 | {"name", "nimoc"}, 867 | }, 868 | } 869 | raw := qb.SQLInsert() 870 | assert.Equal(t, "INSERT IGNORE INTO `user` (`name`) VALUES (?)", raw.Query) 871 | assert.Equal(t, []interface{}{"nimoc"}, raw.Values) 872 | } 873 | 874 | func (suite TestQBSuite) TestInsertMultiple() { 875 | t := suite.T() 876 | qb := sq.QB{ 877 | From: &User{}, 878 | InsertMultiple: sq.InsertMultiple{ 879 | Column: []sq.Column{"name", "age"}, 880 | Values: [][]interface{}{ 881 | {"nimo", 18}, 882 | {"tim", 28}, 883 | }, 884 | }, 885 | } 886 | raw := qb.SQLInsert() 887 | assert.Equal(t, "INSERT INTO `user` (`name`,`age`) VALUES (?,?),(?,?)", raw.Query) 888 | assert.Equal(t, []interface{}{"nimo", 18, "tim", 28}, raw.Values) 889 | } 890 | 891 | func (suite TestQBSuite) TestUpdate() { 892 | t := suite.T() 893 | qb := sq.QB{ 894 | From: &User{}, 895 | UseUpdateIgnore: true, 896 | Set: sq.Set("age", 2), 897 | Where: sq.And("id", sq.Equal(1)), 898 | } 899 | raw := qb.SQLUpdate() 900 | assert.Equal(t, "UPDATE IGNORE `user` SET `age` = ? WHERE `id` = ? AND `deleted_at` IS NULL", raw.Query) 901 | assert.Equal(t, []interface{}{2, 1}, raw.Values) 902 | } 903 | func (suite TestQBSuite) TestSetMap() { 904 | t := suite.T() 905 | assert.Equal(t, sq.SetMap(map[sq.Column]interface{}{ 906 | sq.Column("age"): "a", 907 | sq.Column("id"): "c", 908 | }), sq.OnlyUseInTestToUpdates(t, []sq.Update{ 909 | {Column: "age", Value: "a"}, 910 | {Column: "id", Value: "c"}, 911 | })) 912 | } 913 | func (suite TestQBSuite) TestDebug() { 914 | t := suite.T() 915 | qb := sq.QB{ 916 | From: &User{}, 917 | Where: sq. 918 | And("id", sq.Equal(1)). 919 | And("date", sq.Equal(time.Now())), 920 | Debug: true, 921 | } 922 | qb.SQLSelect() 923 | _ = t 924 | } 925 | func (suite TestQBSuite) TestSet() { 926 | t := suite.T() 927 | { 928 | update := sq.Set("id", sq.Equal(1)) 929 | assert.Equal(t, update[0].Value, 1) 930 | } 931 | { 932 | update := sq.Set("id", sq.Equal(2)) 933 | assert.Equal(t, update[0].Value, 2) 934 | } 935 | } 936 | 937 | func (suite TestQBSuite) TestGroupByOrderBy() { 938 | t := suite.T() 939 | qb := sq.QB{ 940 | From: &User{}, 941 | GroupBy: []sq.Column{"date"}, 942 | WhereAllowEmpty: true, 943 | OrderBy: []sq.OrderBy{ 944 | {"date", sq.DESC}, 945 | }, 946 | } 947 | raw := qb.SQLSelect() 948 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL GROUP BY `date` ORDER BY `date` DESC", raw.Query) 949 | } 950 | 951 | func (suite TestQBSuite) TestBetween() { 952 | t := suite.T() 953 | qb := sq.QB{ 954 | From: &User{}, 955 | Where: sq.And("age", sq.Between(1, 2)), 956 | } 957 | raw := qb.SQLSelect() 958 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` BETWEEN ? AND ? AND `deleted_at` IS NULL", raw.Query) 959 | assert.Equal(t, raw.Values, []interface{}{1, 2}) 960 | } 961 | 962 | func (suite TestQBSuite) TestNotBetween() { 963 | t := suite.T() 964 | qb := sq.QB{ 965 | From: &User{}, 966 | Where: sq.And("age", sq.NotBetween(1, 2)), 967 | } 968 | raw := qb.SQLSelect() 969 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `age` NOT BETWEEN ? AND ? AND `deleted_at` IS NULL", raw.Query) 970 | assert.Equal(t, raw.Values, []interface{}{1, 2}) 971 | } 972 | func (suite TestQBSuite) TestWhereAllowEmpty() { 973 | t := suite.T() 974 | qb := sq.QB{ 975 | From: &User{}, 976 | WhereAllowEmpty: true, 977 | } 978 | raw := qb.SQLSelect() 979 | assert.Equal(t, "SELECT `id`, `name`, `age`, `created_at`, `updated_at` FROM `user` WHERE `deleted_at` IS NULL", raw.Query) 980 | } 981 | 982 | func (suite TestQBSuite) TestPlaceholderSlice() { 983 | t := suite.T() 984 | assert.Equal(t, sq.PlaceholderSlice([]string{"a", "b"}), "(?,?)") 985 | assert.Equal(t, sq.PlaceholderSlice([]string{}), "(NULL)") 986 | assert.Equal(t, sq.PlaceholderSlice([]string{}), "(NULL)") 987 | } 988 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | xerr "github.com/goclub/error" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | type Publish struct { 11 | BusinessID uint64 12 | NextConsumeTime time.Duration 13 | MaxConsumeChance uint16 14 | Priority uint8 `default:"100"` 15 | } 16 | 17 | func corePublishMessage(ctx context.Context, queueTimeLocation *time.Location, s interface { 18 | InsertModel(ctx context.Context, ptr Model, qb QB) (err error) 19 | }, queueName string, publish Publish) (message Message, err error) { 20 | if queueName == "" { 21 | err = xerr.New("goclub/sql: PublishMessage(ctx, queueName, publish) queue can not be empty string") 22 | return 23 | } 24 | if publish.Priority == 0 { 25 | publish.Priority = 100 26 | } 27 | message = Message{ 28 | QueueName: queueName, 29 | BusinessID: publish.BusinessID, 30 | Priority: publish.Priority, 31 | NextConsumeTime: time.Now().In(queueTimeLocation).Add(publish.NextConsumeTime), 32 | ConsumeChance: 0, 33 | MaxConsumeChance: publish.MaxConsumeChance, 34 | UpdateID: "", 35 | } 36 | err = s.InsertModel(ctx, &message, QB{}) 37 | if err != nil { 38 | return 39 | } 40 | return 41 | } 42 | func (db *Database) PublishMessage(ctx context.Context, queueName string, publish Publish) (message Message, err error) { 43 | return corePublishMessage(ctx, db.QueueTimeLocation, db, queueName, publish) 44 | } 45 | func (tx *T) PublishMessage(ctx context.Context, queueName string, publish Publish) (message Message, err error) { 46 | return corePublishMessage(ctx, tx.db.QueueTimeLocation, tx, queueName, publish) 47 | } 48 | 49 | type Consume struct { 50 | QueueName string 51 | HandleError func(err error) 52 | HandleMessage func(message Message) MessageResult 53 | NextConsumeTime func(consumeChance uint16, maxConsumeChance uint16) time.Duration 54 | queueTimeLocation *time.Location 55 | } 56 | 57 | func (data *Consume) initAndCheck(db *Database) (err error) { 58 | data.queueTimeLocation = db.QueueTimeLocation 59 | if data.NextConsumeTime == nil { 60 | data.NextConsumeTime = func(consumeChance uint16, maxConsumeChance uint16) time.Duration { 61 | return time.Minute 62 | } 63 | } 64 | if data.HandleMessage == nil { 65 | return xerr.New("goclub/sql: Database{}.ConsumeMessage(ctx, consume) consume.HandleMessage can not be nil") 66 | } 67 | if data.HandleError == nil { 68 | return xerr.New("goclub/sql: Database{}.ConsumeMessage(ctx, consume) consume.HandleError can not be nil") 69 | } 70 | return 71 | } 72 | func (db *Database) InitQueue(ctx context.Context, queueName string) (err error) { 73 | createQueueTableSQL := "CREATE TABLE IF NOT EXISTS `queue_" + queueName + "` (" + ` 74 | id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 75 | business_id bigint(20) unsigned NOT NULL, 76 | priority tinyint(3) unsigned NOT NULL, 77 | update_id char(24) NOT NULL, 78 | consume_chance smallint(6) unsigned NOT NULL, 79 | max_consume_chance smallint(6) unsigned NOT NULL, 80 | next_consume_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 81 | create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 82 | PRIMARY KEY (id), 83 | KEY business_id (business_id), 84 | KEY update_id (update_id), 85 | KEY next_consume_time__consume_chance__priority (next_consume_time,consume_chance,max_consume_chance,priority) 86 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;` 87 | _, err = db.Exec(ctx, createQueueTableSQL, nil) // indivisible begin 88 | if err != nil { // indivisible end 89 | return err 90 | } 91 | createDeadLetterTableSQL := "CREATE TABLE IF NOT EXISTS `queue_" + queueName + "_dead_letter` (" + ` 92 | id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 93 | business_id bigint(20) unsigned NOT NULL, 94 | reason varchar(255) NOT NULL DEFAULT '', 95 | handled tinyint(3) unsigned NOT NULL, 96 | handled_result varchar(255) NOT NULL, 97 | create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 98 | PRIMARY KEY (id), 99 | KEY business_id (business_id), 100 | KEY create_time (create_time) 101 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;` 102 | _, err = db.Exec(ctx, createDeadLetterTableSQL, nil) // indivisible begin 103 | if err != nil { // indivisible end 104 | return err 105 | } 106 | return 107 | } 108 | func (db *Database) ConsumeMessage(ctx context.Context, consume Consume) error { 109 | err := consume.initAndCheck(db) // indivisible begin 110 | if err != nil { // indivisible end 111 | return err 112 | } 113 | readInterval := time.Second 114 | 115 | for { 116 | time.Sleep(readInterval) 117 | consumed, err := db.tryReadQueueMessage(ctx, consume) // indivisible begin 118 | if err != nil { // indivisible end 119 | consumed = false 120 | consume.HandleError(err) 121 | } 122 | if consumed { 123 | readInterval = time.Nanosecond 124 | } else { 125 | readInterval = time.Second 126 | } 127 | } 128 | } 129 | func (db *Database) tryReadQueueMessage(ctx context.Context, consume Consume) (consumed bool, err error) { 130 | message := Message{QueueName: consume.QueueName} 131 | message.consume = consume 132 | var queueIDs []uint64 133 | // 查询10个id 134 | err = db.QuerySliceScaner(ctx, QB{ 135 | From: &message, 136 | Select: []Column{"id"}, 137 | Where: AndRaw(`next_consume_time <= ?`, time.Now().In(db.QueueTimeLocation)). 138 | AndRaw(`consume_chance < max_consume_chance`), 139 | OrderBy: []OrderBy{ 140 | {"priority", DESC}, 141 | }, 142 | Limit: 10, 143 | }, ScanUint64s(&queueIDs)) 144 | if err != nil { 145 | return 146 | } 147 | // 无结果则退出更新 148 | if len(queueIDs) == 0 { 149 | return 150 | } 151 | updateID := NanoID24() 152 | // 通过更新并发安全的标记数据 (使用where id = 进行更新,避免并发事务死锁) 153 | change, err := db.UpdateAffected(ctx, &message, QB{ 154 | Index: "update_id", 155 | Set: Set("update_id", updateID). 156 | SetRaw(`consume_chance = consume_chance + ?`, 1). 157 | // 先将下次消费时间固定更新到10分钟后避免后续进程中断或sql执行失败导致被重复消费 158 | Set("next_consume_time", time.Now().In(db.QueueTimeLocation).Add(time.Minute*10)), 159 | Where: And("id", In(queueIDs)).AndRaw("consume_chance < max_consume_chance"), 160 | OrderBy: []OrderBy{ 161 | {"priority", DESC}, 162 | }, 163 | Limit: 1, 164 | }) // indivisible begin 165 | if err != nil { // indivisible end 166 | return 167 | } 168 | // 无结果则退出更新 169 | if change == 0 { 170 | return 171 | } 172 | // 查询完整queue数据 173 | hasUpdateMessage, err := db.Query(ctx, &message, QB{ 174 | Where: And("update_id", Equal(updateID)), 175 | Limit: 1, 176 | }) // indivisible begin 177 | if err != nil { // indivisible end 178 | return 179 | } 180 | if hasUpdateMessage == false { 181 | err = xerr.New("goclub/sql: unexpected: Database{}.ConsumeMessage(): update_id(" + updateID + ") should has") 182 | return 183 | } 184 | consumed = true 185 | mqResult := consume.HandleMessage(message) 186 | if mqResult.err != nil { 187 | consume.HandleError(mqResult.err) 188 | } 189 | var execErr error 190 | if mqResult.ack { 191 | if execErr = message.execAck(db); execErr != nil { 192 | consume.HandleError(execErr) 193 | } 194 | } else if mqResult.requeue { 195 | if execErr = message.execRequeue(db, mqResult.requeueDelay); execErr != nil { 196 | consume.HandleError(execErr) 197 | } 198 | } else if mqResult.deadLetter { 199 | if execErr = message.execDeadLetter(db, mqResult.deadLetterReason); execErr != nil { 200 | consume.HandleError(execErr) 201 | } 202 | } else { 203 | consume.HandleError(xerr.New("consume.HandleMessage not allow return empty MessageRequest,messageID:" + strconv.FormatUint(message.ID, 10))) 204 | } 205 | return 206 | } 207 | -------------------------------------------------------------------------------- /queue_dead_letter.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type DeadLetterHandler interface { 9 | // // DeleteDeadLetter 从死信队列中删除消息。 10 | // DeleteDeadLetter(ctx context.Context, id uint64) (err error) 11 | 12 | // HandleDeadLetter 将死信消息标记为已处理。 13 | HandleDeadLetter(ctx context.Context, id uint64, remark string) (err error) 14 | // RequeueDeadLetter 将死信消息重新入队以重新处理。 15 | RequeueDeadLetter(ctx context.Context, id uint64, publish Publish) (err error) 16 | // ArchiveHandledDeadLetter 将已处理过的死信消息归档以备将来分析 17 | ArchiveHandledDeadLetter(ctx context.Context, ago time.Duration) (cleanCount bool, err error) 18 | } 19 | 20 | type DeadLetterQueueMessage struct { 21 | QueueName string 22 | ID uint64 `db:"id" sq:"ignoreInsert"` 23 | BusinessID uint64 `db:"business_id"` 24 | Reason string `db:"reason"` 25 | Handled bool `db:"handled"` 26 | HandledResult string `db:"handled_result"` 27 | CreateTime time.Time `db:"create_time"` 28 | DefaultLifeCycle 29 | WithoutSoftDelete 30 | } 31 | 32 | func (q *DeadLetterQueueMessage) TableName() string { 33 | return "queue_" + q.QueueName + "_dead_letter" 34 | } 35 | 36 | func (v *DeadLetterQueueMessage) AfterInsert(result Result) error { 37 | id, err := result.LastInsertUint64Id() 38 | if err != nil { 39 | return err 40 | } 41 | v.ID = id 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /queue_message.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | xerr "github.com/goclub/error" 7 | "time" 8 | ) 9 | 10 | type Message struct { 11 | QueueName string 12 | ID uint64 `db:"id" sq:"ignoreInsert"` 13 | BusinessID uint64 `db:"business_id"` 14 | NextConsumeTime time.Time `db:"next_consume_time"` 15 | ConsumeChance uint16 `db:"consume_chance"` 16 | MaxConsumeChance uint16 `db:"max_consume_chance"` 17 | UpdateID string `db:"update_id"` 18 | Priority uint8 `db:"priority"` 19 | CreateTime time.Time `db:"create_time"` 20 | consume Consume 21 | DefaultLifeCycle 22 | WithoutSoftDelete 23 | } 24 | 25 | func (message *Message) TableName() string { 26 | return "queue_" + message.QueueName 27 | } 28 | func (v *Message) AfterInsert(result Result) error { 29 | id, err := result.LastInsertUint64Id() 30 | if err != nil { 31 | return err 32 | } 33 | v.ID = uint64(id) 34 | return nil 35 | } 36 | 37 | type MessageResult struct { 38 | ack bool 39 | requeue bool 40 | requeueDelay time.Duration 41 | deadLetter bool 42 | deadLetterReason string 43 | err error 44 | } 45 | 46 | func (v MessageResult) WithError(err error) MessageResult { 47 | if err != nil { 48 | if v.err == nil { 49 | v.err = err 50 | } else { 51 | v.err = xerr.WrapPrefix(err.Error(), err) 52 | } 53 | } 54 | return v 55 | } 56 | func (Message) Ack() MessageResult { 57 | return MessageResult{ 58 | ack: true, 59 | } 60 | } 61 | func (Message) RequeueDelay(duration time.Duration, err error) MessageResult { 62 | return MessageResult{ 63 | requeue: true, 64 | requeueDelay: duration, 65 | err: err, 66 | } 67 | } 68 | func (Message) Requeue(err error) MessageResult { 69 | return MessageResult{ 70 | requeue: true, 71 | err: err, 72 | } 73 | } 74 | func (Message) DeadLetter(reason string, err error) MessageResult { 75 | return MessageResult{ 76 | deadLetter: true, 77 | deadLetterReason: reason, 78 | err: err, 79 | } 80 | } 81 | func (message Message) execAck(db *Database) (err error) { 82 | ctx := context.Background() 83 | if err = db.HardDelete(ctx, &message, QB{ 84 | Where: And("id", Equal(message.ID)), 85 | Limit: 1, 86 | }); err != nil { 87 | return 88 | } 89 | return 90 | } 91 | 92 | func (message Message) execRequeue(db *Database, delay time.Duration) (err error) { 93 | ctx := context.Background() 94 | if message.ConsumeChance == message.MaxConsumeChance { 95 | return message.execDeadLetter(db, "MAX_CONSUME_CHANCE") 96 | } 97 | nextConsumeDuration := delay 98 | if nextConsumeDuration == 0 { 99 | nextConsumeDuration = message.consume.NextConsumeTime(message.ConsumeChance, message.MaxConsumeChance) 100 | } 101 | if err = db.Update(ctx, &message, QB{ 102 | Where: And("id", Equal(message.ID)), 103 | Set: Set("next_consume_time", time.Now().In(message.consume.queueTimeLocation).Add(nextConsumeDuration)), 104 | Limit: 1, 105 | }); err != nil { 106 | return 107 | } 108 | return 109 | } 110 | func (message Message) execDeadLetter(db *Database, reason string) (err error) { 111 | ctx := context.Background() 112 | var rollbackNoError bool 113 | if rollbackNoError, err = db.Begin(ctx, sql.LevelReadCommitted, func(tx *T) TxResult { 114 | if err = tx.HardDelete(ctx, &message, QB{ 115 | Where: And("id", Equal(message.ID)), 116 | Limit: 1, 117 | }); err != nil { // indivisible end 118 | return tx.RollbackWithError(err) 119 | } 120 | if err = tx.InsertModel(ctx, &DeadLetterQueueMessage{ 121 | QueueName: message.QueueName, 122 | BusinessID: message.BusinessID, 123 | Reason: reason, 124 | }, QB{}); err != nil { // indivisible end 125 | return tx.RollbackWithError(err) 126 | } 127 | return tx.Commit() 128 | }); err != nil { 129 | return 130 | } 131 | if rollbackNoError { 132 | return xerr.New("unexpected rollbackNoError") 133 | } 134 | return 135 | } 136 | -------------------------------------------------------------------------------- /queue_test.go: -------------------------------------------------------------------------------- 1 | package sq_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | xerr "github.com/goclub/error" 7 | sq "github.com/goclub/sql" 8 | "github.com/stretchr/testify/assert" 9 | "log" 10 | "math/rand" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestQueueMessage(t *testing.T) { 16 | log.Print("skip TestQueueMessage (return)") 17 | return 18 | func() struct{} { 19 | // ------------- 20 | var err error 21 | _ = err 22 | ctx := context.Background() 23 | _ = ctx 24 | err = func() (err error) { 25 | db := testDB 26 | ctx := context.Background() 27 | db.QueueTimeLocation = time.FixedZone("CST", 8*3600) 28 | queueName := "send_email" 29 | err = db.InitQueue(ctx, queueName) // indivisible begin 30 | if err != nil { // indivisible end 31 | return err 32 | } 33 | // 发布消息 34 | rollbackNoError, err := db.Begin(ctx, sql.LevelReadCommitted, func(tx *sq.T) sq.TxResult { 35 | _, err := tx.PublishMessage(ctx, queueName, sq.Publish{ 36 | NextConsumeTime: time.Nanosecond, 37 | BusinessID: 1, 38 | MaxConsumeChance: 3, 39 | }) 40 | if err != nil { 41 | return tx.RollbackWithError(err) 42 | } 43 | // 插入消息详细内容(不同的业务场景详细内容不一样) 44 | // tx.InsertModel(ctx, &QueueSendEmailBody, sq.QB{}) 45 | return tx.Commit() 46 | }) 47 | if err != nil { 48 | return 49 | } 50 | if rollbackNoError { 51 | return xerr.New("unexpected rollback no error") 52 | } 53 | 54 | // 消费消息 55 | consume := sq.Consume{ 56 | QueueName: "send_email", 57 | NextConsumeTime: func(consumeChance uint16, maxConsumeChance uint16) time.Duration { 58 | return time.Second * 3 59 | }, 60 | HandleError: func(err error) { 61 | // 消费时产生的错误应当记录,而不是退出程序 62 | // 打印错误或将错误发送到 sentry 63 | log.Printf("%+v", err) 64 | }, 65 | HandleMessage: func(message sq.Message) sq.MessageResult { 66 | log.Print("consume message:", message.ID) 67 | random := rand.Uint64() % 3 // 0 1 2 68 | // random = 1 69 | switch random { 70 | // 确认并删除消息 71 | case 0: 72 | log.Print("ack message:", message.ID) 73 | return message.Ack() 74 | // 退回到队列稍后再消费 75 | case 1: 76 | log.Print("requeue message:", message.ID) 77 | return message.Requeue(nil) // indivisible begin 78 | // 删除消息并记录到死信队列 79 | default: 80 | log.Print("deadLetter message:", message.ID) 81 | return message.DeadLetter("进入死信的原因", nil) 82 | } 83 | }, 84 | } 85 | err = db.ConsumeMessage(ctx, consume) 86 | if err != nil { 87 | return 88 | } 89 | return 90 | }() 91 | // indivisible begin 92 | assert.NoError(t, err) // indivisible end 93 | // ------------- 94 | return struct{}{} 95 | }() 96 | } 97 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "fmt" 5 | xerr "github.com/goclub/error" 6 | prettyTable "github.com/jedib0t/go-pretty/v6/table" 7 | "github.com/jedib0t/go-pretty/v6/text" 8 | "github.com/jmoiron/sqlx" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | var renderStyle prettyTable.Style 14 | 15 | func init() { 16 | renderStyle = prettyTable.StyleColoredBlackOnBlueWhite 17 | renderStyle.Color.Header = text.Colors{text.BgHiBlack, text.FgHiWhite} 18 | renderStyle.Format.Header = text.FormatDefault 19 | renderStyle.Color.RowAlternate = renderStyle.Color.Row 20 | } 21 | func renderLastQueryCost(debugID uint64, lastQueryCost float64) (render string) { 22 | t := prettyTable.NewWriter() 23 | t.AppendHeader(prettyTable.Row{"LastQueryCost" + " (" + strconv.FormatUint(debugID, 10) + ")"}) 24 | t.AppendRow([]interface{}{strconv.FormatFloat(lastQueryCost, 'f', -1, 64)}) 25 | t.SetStyle(renderStyle) 26 | return "\n" + t.Render() 27 | } 28 | func renderRunTime(debugID uint64, duration time.Duration) (render string) { 29 | t := prettyTable.NewWriter() 30 | t.AppendHeader(prettyTable.Row{"RunTime" + " (" + strconv.FormatUint(debugID, 10) + ")"}) 31 | t.AppendRow([]interface{}{duration.String()}) 32 | t.SetStyle(renderStyle) 33 | return "\n" + t.Render() 34 | } 35 | func renderSQL(debugID uint64, query string, values []interface{}) (render string) { 36 | var printValues string 37 | for _, value := range values { 38 | printValues = printValues + fmt.Sprintf("%T(%v) ", value, value) + " " 39 | } 40 | t := prettyTable.NewWriter() 41 | t.AppendHeader(prettyTable.Row{"PrintSQL" + " (" + strconv.FormatUint(debugID, 10) + ")"}, prettyTable.RowConfig{AutoMerge: true}) 42 | t.AppendRow(prettyTable.Row{query}, prettyTable.RowConfig{AutoMerge: true}) 43 | t.AppendRow([]interface{}{printValues}) 44 | t.SetStyle(renderStyle) 45 | return "\n" + t.Render() 46 | } 47 | func renderExplain(debugID uint64, row *sqlx.Row) (render string) { 48 | var err error 49 | defer func() { 50 | if err != nil { 51 | xerr.PrintStack(err) 52 | } 53 | }() 54 | t := prettyTable.NewWriter() 55 | t.AppendHeader(prettyTable.Row{"Explain" + " (" + strconv.FormatUint(debugID, 10) + ")"}, prettyTable.RowConfig{AutoMerge: true}) 56 | t.AppendRow(prettyTable.Row{"id", "select_type", "table", "partitions", 57 | "type", "possible_keys", "key", "key_len", "ref", 58 | "rows", "filtered", "Extra"}) 59 | var id, selectType, table, partitions, ttype, possibleLeys, key, keyLen, ref, rows, filtered, Extra *string 60 | err = row.Scan(&id, &selectType, &table, &partitions, 61 | &ttype, &possibleLeys, &key, &keyLen, &ref, 62 | &rows, &filtered, &Extra) 63 | _, err = CheckRowScanErr(err) 64 | if err != nil { 65 | return 66 | } 67 | sn := func(v *string) string { 68 | if v == nil { 69 | return "NULL" 70 | } 71 | return *v 72 | } 73 | t.AppendRow(prettyTable.Row{ 74 | sn(id), sn(selectType), sn(table), sn(partitions), 75 | sn(ttype), sn(possibleLeys), sn(key), sn(keyLen), sn(ref), 76 | sn(rows), sn(filtered), sn(Extra), 77 | }) 78 | t.SetStyle(renderStyle) 79 | return "\n" + t.Render() 80 | } 81 | func renderReview(debugID uint64, query string, reviews []string, refs string) (render string) { 82 | { 83 | t := prettyTable.NewWriter() 84 | t.SetStyle(renderStyle) 85 | t.AppendHeader(prettyTable.Row{"Review" + " (" + strconv.FormatUint(debugID, 10) + ")"}, prettyTable.RowConfig{AutoMerge: true}) 86 | render = render + "\n" + t.Render() 87 | } 88 | { 89 | t := prettyTable.NewWriter() 90 | t.SetStyle(renderStyle) 91 | t.AppendHeader(prettyTable.Row{"Execute"}, prettyTable.RowConfig{AutoMerge: true}) 92 | t.AppendRow(prettyTable.Row{query}, prettyTable.RowConfig{AutoMerge: true}) 93 | render = render + "\n" + t.Render() 94 | } 95 | { 96 | t := prettyTable.NewWriter() 97 | t.SetStyle(renderStyle) 98 | t.AppendHeader(prettyTable.Row{"Reviews"}, prettyTable.RowConfig{AutoMerge: true}) 99 | for _, review := range reviews { 100 | t.AppendRow(prettyTable.Row{review}) 101 | } 102 | render = render + "\n" + t.Render() 103 | } 104 | return render 105 | } 106 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "database/sql" 5 | xerr "github.com/goclub/error" 6 | ) 7 | 8 | type Result struct { 9 | core sql.Result 10 | } 11 | 12 | func (r Result) LastInsertId() (id int64, err error) { 13 | id, err = r.core.LastInsertId() 14 | if err != nil { 15 | err = xerr.WithStack(err) 16 | return 17 | } 18 | return 19 | } 20 | func (r Result) LastInsertUint64Id() (id uint64, err error) { 21 | var int64id int64 22 | int64id, err = r.LastInsertId() 23 | if err != nil { 24 | return 25 | } 26 | if int64id < 0 { 27 | err = xerr.New("goclub/sql: sq.Result{}.LastInsertUint64Id() (id, err) id less than 0") 28 | return 29 | } 30 | id = uint64(int64id) 31 | return 32 | } 33 | func (r Result) RowsAffected() (rowsAffected int64, err error) { 34 | rowsAffected, err = r.core.RowsAffected() 35 | if err != nil { 36 | err = xerr.WithStack(err) 37 | return 38 | } 39 | return 40 | } 41 | 42 | func RowsAffected(result Result, execErr error) (affected int64, err error) { 43 | if execErr != nil { 44 | err = execErr 45 | return 46 | } 47 | return result.RowsAffected() 48 | } 49 | -------------------------------------------------------------------------------- /scan_func.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "github.com/jmoiron/sqlx" 5 | "time" 6 | ) 7 | 8 | type Scaner func(rows *sqlx.Rows) error 9 | 10 | type UintLister interface { 11 | Append(i uint) 12 | } 13 | 14 | func ScanUintLister(list UintLister) Scaner { 15 | return func(rows *sqlx.Rows) error { 16 | var item uint 17 | err := rows.Scan(&item) 18 | if err != nil { 19 | return err 20 | } 21 | list.Append(item) 22 | return nil 23 | } 24 | } 25 | 26 | type IntLister interface { 27 | Append(i int) 28 | } 29 | 30 | func ScanIntLister(list IntLister) Scaner { 31 | return func(rows *sqlx.Rows) error { 32 | var item int 33 | err := rows.Scan(&item) 34 | if err != nil { 35 | return err 36 | } 37 | list.Append(item) 38 | return nil 39 | } 40 | } 41 | 42 | type BytesIDLister interface { 43 | Append(data []byte) 44 | } 45 | 46 | func ScanBytesLister(list BytesIDLister) Scaner { 47 | return func(rows *sqlx.Rows) error { 48 | var item []byte 49 | err := rows.Scan(&item) 50 | if err != nil { 51 | return err 52 | } 53 | list.Append(item) 54 | return nil 55 | } 56 | } 57 | 58 | type StringLister interface { 59 | Append(s string) 60 | } 61 | 62 | func ScanStringLister(list StringLister) Scaner { 63 | return func(rows *sqlx.Rows) error { 64 | var item string 65 | err := rows.Scan(&item) 66 | if err != nil { 67 | return err 68 | } 69 | list.Append(item) 70 | return nil 71 | } 72 | } 73 | func ScanBytes(bytes *[][]byte) Scaner { 74 | return func(rows *sqlx.Rows) error { 75 | var item []byte 76 | err := rows.Scan(&item) 77 | if err != nil { 78 | return err 79 | } 80 | *bytes = append(*bytes, item) 81 | return nil 82 | } 83 | } 84 | func ScanStrings(strings *[]string) Scaner { 85 | return func(rows *sqlx.Rows) error { 86 | var item string 87 | err := rows.Scan(&item) 88 | if err != nil { 89 | return err 90 | } 91 | *strings = append(*strings, item) 92 | return nil 93 | } 94 | } 95 | func ScanInts(ints *[]int) Scaner { 96 | return func(rows *sqlx.Rows) error { 97 | var item int 98 | err := rows.Scan(&item) 99 | if err != nil { 100 | return err 101 | } 102 | *ints = append(*ints, item) 103 | return nil 104 | } 105 | } 106 | func ScanUints(ints *[]uint) Scaner { 107 | return func(rows *sqlx.Rows) error { 108 | var item uint 109 | err := rows.Scan(&item) 110 | if err != nil { 111 | return err 112 | } 113 | *ints = append(*ints, item) 114 | return nil 115 | } 116 | } 117 | func ScanInt64s(ints *[]int64) Scaner { 118 | return func(rows *sqlx.Rows) error { 119 | var item int64 120 | err := rows.Scan(&item) 121 | if err != nil { 122 | return err 123 | } 124 | *ints = append(*ints, item) 125 | return nil 126 | } 127 | } 128 | func ScanUint64s(ints *[]uint64) Scaner { 129 | return func(rows *sqlx.Rows) error { 130 | var item uint64 131 | err := rows.Scan(&item) 132 | if err != nil { 133 | return err 134 | } 135 | *ints = append(*ints, item) 136 | return nil 137 | } 138 | } 139 | func ScanBool(bools *[]bool) Scaner { 140 | return func(rows *sqlx.Rows) error { 141 | var item bool 142 | err := rows.Scan(&item) 143 | if err != nil { 144 | return err 145 | } 146 | *bools = append(*bools, item) 147 | return nil 148 | } 149 | } 150 | func ScanTimes(times *[]time.Time) Scaner { 151 | return func(rows *sqlx.Rows) error { 152 | var item time.Time 153 | err := rows.Scan(&item) 154 | if err != nil { 155 | return err 156 | } 157 | *times = append(*times, item) 158 | return nil 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /sql_checker.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | xerr "github.com/goclub/error" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type SQLChecker interface { 10 | Check(checkSQL []string, execSQL string) (pass bool, refs string, err error) 11 | TrackFail(debugID uint64, err error, reviews []string, query string, refs string) 12 | } 13 | 14 | type DefaultSQLChecker struct { 15 | } 16 | 17 | func (check DefaultSQLChecker) Check(reviews []string, query string) (pass bool, refs string, err error) { 18 | if len(reviews) == 0 { 19 | return true, "", nil 20 | } 21 | for _, s := range reviews { 22 | if s == query { 23 | return true, "", nil 24 | } 25 | } 26 | for _, format := range reviews { 27 | matched, ref, err := check.match(query, format) 28 | if err != nil { 29 | return false, refs, err 30 | } 31 | refs += ref 32 | if matched == true { 33 | return true, "", nil 34 | } 35 | } 36 | return false, refs, nil 37 | } 38 | func (check DefaultSQLChecker) TrackFail(debugID uint64, err error, reviews []string, query string, refs string) { 39 | Log.Warn("DefaultSQLChecker Fail\n" + renderReview(debugID, query, reviews, refs)) 40 | } 41 | 42 | type defaultSQLCheckerDifferent struct { 43 | match bool 44 | trimmedSQL string 45 | trimmedFormat string 46 | } 47 | 48 | func (check DefaultSQLChecker) match(query string, format string) (matched bool, ref string, err error) { 49 | trimmedFormat := format 50 | 51 | trimmedSQL := query 52 | // remove {#VALUES#} 和 (?,?),(?,?) 和 (?,?) 53 | { 54 | var reg *regexp.Regexp 55 | reg, err = regexp.Compile(`VALUES \(.*\)`) 56 | if err != nil { 57 | return 58 | } 59 | trimmedSQL = reg.ReplaceAllString(trimmedSQL, "VALUES ") 60 | trimmedFormat = strings.Replace(trimmedFormat, "{#VALUES#}", "", -1) 61 | } 62 | // remove {#IN#} 和 (?,?) 63 | { 64 | var reg *regexp.Regexp 65 | reg, err = regexp.Compile(`\(\?(,\?)*?\)`) 66 | if err != nil { 67 | return 68 | } 69 | trimmedSQL = reg.ReplaceAllString(trimmedSQL, "") 70 | trimmedFormat = strings.Replace(trimmedFormat, "{#IN#}", "", -1) 71 | } 72 | optional, err := check.matchCheckSQLOptional(trimmedFormat) 73 | if err != nil { 74 | return 75 | } 76 | for _, optionalItem := range optional { 77 | trimmedFormat = strings.Replace(trimmedFormat, "{#"+optionalItem+"#}", "", 1) 78 | } 79 | for _, optionalItem := range optional { 80 | trimmedSQL = strings.Replace(trimmedSQL, optionalItem, "", 1) 81 | } 82 | trimmedFormat = strings.TrimSpace(trimmedFormat) 83 | trimmedSQL = strings.TrimSpace(trimmedSQL) 84 | if trimmedSQL == trimmedFormat { 85 | return true, "", nil 86 | } 87 | ref = "\n sql: \"" + trimmedSQL + "\"\nformat: \"" + trimmedFormat + "\"" 88 | return 89 | } 90 | 91 | // 匹配 QB{}.Review 中的 {# AND `name` = ?#} 部分并返回 92 | func (check DefaultSQLChecker) matchCheckSQLOptional(str string) (optional []string, err error) { 93 | strLen := len(str) 94 | type Position struct { 95 | Start int 96 | End int 97 | Done bool 98 | } 99 | data := []Position{} 100 | for index, s := range str { 101 | switch s { 102 | case []rune("{")[0]: 103 | // last rune 104 | if index == strLen-1 { 105 | continue 106 | } 107 | nextRune := str[index+1] 108 | if nextRune == []byte("#")[0] { 109 | // 检查之前是否出现 {# 但没有 #} 这种错误 110 | if len(data) != 0 { 111 | last := data[len(data)-1] 112 | if last.Done == false { 113 | message := "goclub/sql: SQLCheck missing #}\n" + str + "\n" + 114 | strings.Repeat(" ", index) + "^" 115 | return nil, xerr.New(message) 116 | } 117 | } 118 | data = append(data, Position{ 119 | Start: index, 120 | }) 121 | } 122 | case []rune("#")[0]: 123 | // last rune 124 | if index == strLen-1 { 125 | continue 126 | } 127 | nextRune := str[index+1] 128 | if nextRune == []byte("}")[0] { 129 | endIndex := index + 2 130 | // 检查 #} 之前必须存在 {# 131 | if len(data) == 0 { 132 | return nil, xerr.New("goclub/sq;: SQLCheck missing {#\n" + str + "\n" + 133 | strings.Repeat(" ", index) + "^") 134 | } 135 | last := data[len(data)-1] 136 | if last.Done == true { 137 | message := "goclub/sql: SQLCheck missing {#\n" + str + "\n" + 138 | strings.Repeat(" ", index) + "^" 139 | return nil, xerr.New(message) 140 | } 141 | last.End = endIndex 142 | last.Done = true 143 | data[len(data)-1] = last 144 | } 145 | } 146 | } 147 | for _, item := range data { 148 | if item.Done == false { 149 | message := "goclub/sql: SQLCheck missing #}\n" + str + "\n" + 150 | strings.Repeat(" ", len(str)) + "^" 151 | return nil, xerr.New(message) 152 | } 153 | optional = append(optional, str[item.Start+2:item.End-2]) 154 | } 155 | return 156 | } 157 | -------------------------------------------------------------------------------- /sql_checker_test.go: -------------------------------------------------------------------------------- 1 | package sq_test 2 | 3 | import ( 4 | sq "github.com/goclub/sql" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestDefaultSQLChecker_Check(t *testing.T) { 10 | check := sq.DefaultSQLChecker{} 11 | { 12 | checkSQL := []string{ 13 | "select * from user where id in {#IN#}", 14 | } 15 | { 16 | execSQL := "select * from user where id in (?)" 17 | matched, _, err := check.Check(checkSQL, execSQL) 18 | assert.Equal(t, matched, true) 19 | assert.NoError(t, err) 20 | } 21 | { 22 | execSQL := "select * from user where id in (?,?)" 23 | matched, _, err := check.Check(checkSQL, execSQL) 24 | assert.Equal(t, matched, true) 25 | assert.NoError(t, err) 26 | } 27 | { 28 | execSQL := "select * from user where id in (?,?,?)" 29 | matched, _, err := check.Check(checkSQL, execSQL) 30 | assert.Equal(t, matched, true) 31 | assert.NoError(t, err) 32 | } 33 | { 34 | execSQL := "select * from user where id in (?,?,?,?)" 35 | matched, _, err := check.Check(checkSQL, execSQL) 36 | assert.Equal(t, matched, true) 37 | assert.NoError(t, err) 38 | } 39 | } 40 | { 41 | checkSQL := []string{ 42 | "select * from user where id in {#IN#} limit ?", 43 | } 44 | { 45 | execSQL := "select * from user where id in (?) limit ?" 46 | matched, _, err := check.Check(checkSQL, execSQL) 47 | assert.Equal(t, matched, true) 48 | assert.NoError(t, err) 49 | } 50 | { 51 | execSQL := "select * from user where id in (?,?) limit ?" 52 | matched, _, err := check.Check(checkSQL, execSQL) 53 | assert.Equal(t, matched, true) 54 | assert.NoError(t, err) 55 | } 56 | } 57 | { 58 | checkSQL := []string{ 59 | "select * from user where mobile = ?{# and name = ?#}{# and age = ?#} limit ?", 60 | } 61 | { 62 | execSQL := "select * from user where mobile = ? limit ?" 63 | matched, _, err := check.Check(checkSQL, execSQL) 64 | assert.Equal(t, matched, true) 65 | assert.NoError(t, err) 66 | } 67 | { 68 | execSQL := "select * from user where mobile = ? and name = ? limit ?" 69 | matched, _, err := check.Check(checkSQL, execSQL) 70 | assert.Equal(t, matched, true) 71 | assert.NoError(t, err) 72 | } 73 | { 74 | execSQL := "select * from user where mobile = ? and age = ? limit ?" 75 | matched, _, err := check.Check(checkSQL, execSQL) 76 | assert.Equal(t, matched, true) 77 | assert.NoError(t, err) 78 | } 79 | { 80 | execSQL := "select * from user where mobile = ? and name = ? and age = ? limit ?" 81 | matched, _, err := check.Check(checkSQL, execSQL) 82 | assert.Equal(t, matched, true) 83 | assert.NoError(t, err) 84 | } 85 | { 86 | execSQL := "select * from user" 87 | matched, _, err := check.Check(checkSQL, execSQL) 88 | assert.Equal(t, matched, false) 89 | assert.NoError(t, err) 90 | } 91 | } 92 | } 93 | 94 | func TestDefaultSQLChecker_Check2(t *testing.T) { 95 | check := sq.DefaultSQLChecker{} 96 | { 97 | checkSQL := []string{ 98 | "select * from user where mobile = ? {#and name = ?#} limit ?", 99 | } 100 | { 101 | execSQL := "select * from user where mobile = ? limit ?" 102 | matched, _, err := check.Check(checkSQL, execSQL) 103 | assert.Equal(t, matched, false) 104 | assert.NoError(t, err) 105 | } 106 | } 107 | { 108 | checkSQL := []string{ 109 | "select * from user where mobile = ?{# and name = ?#} limit ?", 110 | } 111 | { 112 | execSQL := "select * from user where mobile = ? limit ?" 113 | matched, _, err := check.Check(checkSQL, execSQL) 114 | assert.Equal(t, matched, true) 115 | assert.NoError(t, err) 116 | } 117 | } 118 | } 119 | func TestDefaultSQLChecker_Check3(t *testing.T) { 120 | check := sq.DefaultSQLChecker{} 121 | { 122 | checkSQL := []string{ 123 | "INSERT INTO `user` (`name`,`age`) VALUES {#VALUES#}", 124 | } 125 | { 126 | execSQL := "INSERT INTO `user` (`name`,`age`) VALUES (?,?),(?,?)" 127 | matched, _, err := check.Check(checkSQL, execSQL) 128 | assert.Equal(t, matched, true) 129 | assert.NoError(t, err) 130 | } 131 | { 132 | execSQL := "INSERT INTO `user` (`name`,`age`) VALUES (?,?)" 133 | matched, _, err := check.Check(checkSQL, execSQL) 134 | assert.Equal(t, matched, true) 135 | assert.NoError(t, err) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | xerr "github.com/goclub/error" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type Tag struct { 10 | Value string 11 | } 12 | 13 | func (t Tag) IsIgnoreInsert() bool { 14 | sqTags := strings.Split(t.Value, "|") 15 | for _, tag := range sqTags { 16 | if strings.TrimSpace(tag) == "ignoreInsert" { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | func TagToColumns(v interface{}) (columns []Column) { 23 | rValue := reflect.ValueOf(v) 24 | rType := rValue.Type() 25 | if rType.Kind() == reflect.Ptr { 26 | rValue = rValue.Elem() 27 | rType = rValue.Type() 28 | } 29 | tier := 0 30 | scanTagToColumns(rValue, rType, &columns, &tier) 31 | return 32 | } 33 | func scanTagToColumns(rValue reflect.Value, rType reflect.Type, columns *[]Column, tier *int) { 34 | if *tier > 10 { 35 | panic(xerr.New("goclub/sql: Too many structures are nested")) 36 | } 37 | for i := 0; i < rType.NumField(); i++ { 38 | structField := rType.Field(i) 39 | tag, has := structField.Tag.Lookup("db") 40 | if !has { 41 | if structField.Type.Kind() == reflect.Struct { 42 | fieldValue := rValue.Field(i) 43 | scanTagToColumns(fieldValue, structField.Type, columns, tier) 44 | // fieldValue 45 | } 46 | continue 47 | } 48 | if tag != "" { 49 | *columns = append(*columns, Column(tag)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | xerr "github.com/goclub/error" 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | type T struct { 11 | Core *sqlx.Tx 12 | db *Database 13 | } 14 | 15 | func (tx *T) getCore() (core StoragerCore) { 16 | return tx.Core 17 | } 18 | func (tx *T) getSQLChecker() (sqlChecker SQLChecker) { 19 | return tx.db.getSQLChecker() 20 | } 21 | func newTx(tx *sqlx.Tx, db *Database) *T { 22 | return &T{tx, db} 23 | } 24 | 25 | // TxResult 26 | // tx.Commit() commit transaction 27 | // tx.Rollback() rollback transaction , rollbackNoError = true 28 | // tx.Error(err) rollback transaction , rollbackNoError = false, err = err 29 | type TxResult struct { 30 | isCommit bool 31 | withError error 32 | } 33 | 34 | func (T) Commit() TxResult { 35 | return TxResult{ 36 | isCommit: true, 37 | } 38 | } 39 | func (T) Rollback() TxResult { 40 | return TxResult{ 41 | isCommit: false, 42 | } 43 | } 44 | func (T) RollbackWithError(err error) TxResult { 45 | return TxResult{ 46 | isCommit: false, 47 | withError: xerr.WithStack(err), 48 | } 49 | } 50 | 51 | // Error same RollbackWithError 52 | func (T) Error(err error) TxResult { 53 | return TxResult{ 54 | isCommit: false, 55 | withError: xerr.WithStack(err), 56 | } 57 | } 58 | 59 | // 给 TxResult 增加 Error 接口是为了避出现类似 tx.Rollback() 前面没有 return 的错误 60 | func (result TxResult) Error() string { 61 | if result.withError != nil { 62 | return result.withError.Error() 63 | } 64 | if result.isCommit { 65 | return "goclub/sql: result commit" 66 | } else { 67 | return "goclub/sql: result rollback" 68 | } 69 | } 70 | func (db *Database) Begin(ctx context.Context, level sql.IsolationLevel, handle func(tx *T) TxResult) (rollbackNoError bool, err error) { 71 | return db.BeginOpt(ctx, sql.TxOptions{ 72 | Isolation: level, 73 | ReadOnly: false, 74 | }, handle) 75 | } 76 | func (db *Database) BeginOpt(ctx context.Context, opt sql.TxOptions, handle func(tx *T) TxResult) (rollbackNoError bool, err error) { 77 | coreTx, err := db.Core.BeginTxx(ctx, &opt) 78 | if err != nil { 79 | return 80 | } 81 | tx := newTx(coreTx, db) 82 | txResult := handle(tx) 83 | if txResult.isCommit { 84 | err = tx.Core.Commit() 85 | if err != nil { 86 | err = xerr.WithStack(err) 87 | return 88 | } 89 | return 90 | } else { 91 | err = tx.Core.Rollback() 92 | if err != nil { 93 | err = xerr.WithStack(err) 94 | return 95 | } 96 | if txResult.withError != nil { 97 | err = txResult.withError 98 | return 99 | } 100 | rollbackNoError = true 101 | return 102 | } 103 | } 104 | 105 | const ( 106 | LevelDefault sql.IsolationLevel = sql.LevelDefault 107 | LevelReadUncommitted sql.IsolationLevel = sql.LevelReadUncommitted 108 | LevelReadCommitted sql.IsolationLevel = sql.LevelReadCommitted 109 | LevelWriteCommitted sql.IsolationLevel = sql.LevelWriteCommitted 110 | LevelRepeatableRead sql.IsolationLevel = sql.LevelRepeatableRead 111 | LevelSnapshot sql.IsolationLevel = sql.LevelSnapshot 112 | LevelSerializable sql.IsolationLevel = sql.LevelSerializable 113 | LevelLinearizable sql.IsolationLevel = sql.LevelLinearizable 114 | ) 115 | 116 | const ( 117 | RC sql.IsolationLevel = sql.LevelReadCommitted 118 | RR sql.IsolationLevel = sql.LevelRepeatableRead 119 | ) 120 | -------------------------------------------------------------------------------- /type_create_update_time.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type CreatedAtUpdatedAt struct { 10 | CreatedAt time.Time `db:"created_at"` 11 | UpdatedAt time.Time `db:"updated_at"` 12 | } 13 | type CreateTimeUpdateTime struct { 14 | CreateTime time.Time `db:"create_time"` 15 | UpdateTime time.Time `db:"update_time"` 16 | } 17 | type GMTCreateGMTModified struct { 18 | GMTCreate time.Time `db:"gmt_create"` 19 | GMTModified time.Time `db:"gmt_modified"` 20 | } 21 | 22 | func setTimeNow(fieldValue reflect.Value, fieldType reflect.StructField) { 23 | if fieldValue.IsZero() { 24 | if fieldType.Type.String() == "time.Time" { 25 | now := time.Now() 26 | if strings.HasPrefix(fieldType.Name, "GMT") { 27 | now = now.In(time.UTC) 28 | } 29 | fieldValue.Set(reflect.ValueOf(now)) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /type_soft_delete.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import "time" 4 | 5 | type WithoutSoftDelete struct{} 6 | 7 | func (WithoutSoftDelete) SoftDeleteWhere() Raw { return Raw{} } 8 | func (WithoutSoftDelete) SoftDeleteSet() Raw { return Raw{} } 9 | 10 | type SoftDeletedAt struct{} 11 | 12 | func (SoftDeletedAt) SoftDeleteWhere() Raw { return Raw{"`deleted_at` IS NULL", nil} } 13 | func (SoftDeletedAt) SoftDeleteSet() Raw { return Raw{"`deleted_at` = ?", []interface{}{time.Now()}} } 14 | 15 | type SoftDeleteTime struct{} 16 | 17 | func (SoftDeleteTime) SoftDeleteWhere() Raw { return Raw{"`delete_time` IS NULL", nil} } 18 | func (SoftDeleteTime) SoftDeleteSet() Raw { return Raw{"`delete_time` = ?", []interface{}{time.Now()}} } 19 | 20 | type SoftIsDeleted struct{} 21 | 22 | func (SoftIsDeleted) SoftDeleteWhere() Raw { return Raw{"`is_deleted` = 0", nil} } 23 | func (SoftIsDeleted) SoftDeleteSet() Raw { return Raw{"`is_deleted` = 1", nil} } 24 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package sq 2 | 3 | import ( 4 | "database/sql" 5 | xerr "github.com/goclub/error" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type stringQueue struct { 11 | Value []string 12 | } 13 | 14 | func (v *stringQueue) Push(args ...string) { 15 | v.Value = append(v.Value, args...) 16 | } 17 | func (v stringQueue) Join(sep string) string { 18 | return strings.Join(v.Value, sep) 19 | } 20 | 21 | type stringQueueBindValue struct { 22 | Value string 23 | Has bool 24 | } 25 | 26 | func (sList *stringQueue) PopBind(last *stringQueueBindValue) stringQueue { 27 | listLen := len(sList.Value) 28 | if listLen == 0 { 29 | /* 30 | Clear StringListBindValue Because in this case 31 | ``` 32 | list.PopBind(&last) 33 | // do Something.. 34 | list.PopBind(&last) 35 | ``` 36 | last test same var 37 | */ 38 | last.Value = "" 39 | last.Has = false 40 | return *sList 41 | } 42 | last.Value = sList.Value[listLen-1] 43 | last.Has = true 44 | sList.Value = sList.Value[:listLen-1] 45 | return *sList 46 | } 47 | func columnsToStrings(columns []Column) (strings []string) { 48 | for _, column := range columns { 49 | strings = append(strings, column.wrapField()) 50 | } 51 | return 52 | } 53 | func columnsToStringsWithAS(columns []Column) (strings []string) { 54 | for _, column := range columns { 55 | strings = append(strings, column.wrapFieldWithAS()) 56 | } 57 | return 58 | } 59 | 60 | type primaryIDInfo struct { 61 | HasID bool 62 | IDValue interface{} 63 | } 64 | 65 | func CheckRowScanErr(scanErr error) (has bool, err error) { 66 | if scanErr != nil { 67 | if scanErr == sql.ErrNoRows { 68 | return false, nil 69 | } else { 70 | return false, scanErr 71 | } 72 | } else { 73 | has = true 74 | } 75 | return 76 | } 77 | 78 | func PlaceholderSlice(slice interface{}) (placeholder string) { 79 | var values []interface{} 80 | rValue := reflect.ValueOf(slice) 81 | if rValue.Type().Kind() != reflect.Slice { 82 | panic(xerr.New("sq.PlaceholderIn(" + rValue.Type().Name() + ") slice must be slice")) 83 | } 84 | if rValue.Len() == 0 { 85 | placeholder = "(NULL)" 86 | } else { 87 | var placeholderList []string 88 | for i := 0; i < rValue.Len(); i++ { 89 | values = append(values, rValue.Index(i).Interface()) 90 | placeholderList = append(placeholderList, "?") 91 | } 92 | placeholder = "(" + strings.Join(placeholderList, ",") + ")" 93 | } 94 | return 95 | } 96 | --------------------------------------------------------------------------------