├── .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 | [](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 | 
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 |
--------------------------------------------------------------------------------