├── internal ├── examples │ ├── example1 │ │ ├── example1.go │ │ └── example1_test.go │ ├── example2 │ │ ├── example2.go │ │ └── example2_test.go │ ├── example3 │ │ ├── example3.go │ │ └── example3_test.go │ └── example4 │ │ ├── example4.go │ │ └── example4_test.go ├── tests │ ├── tests_test.go │ └── tests.go ├── utils │ ├── utils.go │ └── types.go ├── demos │ ├── demo2x │ │ └── main.go │ └── demo1x │ │ └── main.go └── docs │ ├── README_OLD_DOC.zh.md │ ├── README_OLD_DOC.en.md │ ├── CREATION_IDEAS.zh.md │ └── CREATION_IDEAS.en.md ├── Makefile ├── .gitignore ├── gormcnmstub ├── stub.go ├── stub.gen_test.go ├── stub.gen.go └── stub_test.go ├── gorm_scope.go ├── LICENSE ├── op_clause_test.go ├── go.mod ├── select_statement_test.go ├── orders_test.go ├── value_map_test.go ├── statement_args_test.go ├── qs_coalesce_test.go ├── new_column_name_test.go ├── orders.go ├── table_tbjoin.go ├── qs_conjunction_test.go ├── op_clause.go ├── new_column_name.go ├── select_statement.go ├── .github └── workflows │ └── release.yml ├── qs_coalesce.go ├── qs_conjunction.go ├── value_map.go ├── table_column.go ├── qx_conjunction.go ├── cname_test.go ├── gorm_scope_test.go ├── gormcnmjson ├── json.go └── json_test.go ├── qx_conjunction_test.go ├── column_name_test.go ├── op_common_test.go ├── statement_args.go ├── op_common.go ├── go.sum └── column_name.go /internal/examples/example1/example1.go: -------------------------------------------------------------------------------- 1 | // Package example1 provides a placeholder package used in gormcnm test structure 2 | // Auto reserved as namespace definition during test execution 3 | // 4 | // example1 包提供在 gormcnm 测试结构中使用的占位包 5 | // 自动预留作为测试执行期间的命名空间定义 6 | package example1 7 | -------------------------------------------------------------------------------- /internal/examples/example2/example2.go: -------------------------------------------------------------------------------- 1 | // Package example2 provides a placeholder package used in gormcnm test structure 2 | // Auto reserved as namespace definition during test execution 3 | // 4 | // example2 包提供在 gormcnm 测试结构中使用的占位包 5 | // 自动预留作为测试执行期间的命名空间定义 6 | package example2 7 | -------------------------------------------------------------------------------- /internal/tests/tests_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/gormcnm/internal/tests" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | func TestNewDBRun(t *testing.T) { 12 | tests.NewDBRun(t, func(db *gorm.DB) { 13 | var result int 14 | require.NoError(t, db.Raw("SELECT 1").Scan(&result).Error) 15 | require.Equal(t, 1, result) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COVERAGE_DIR ?= .coverage.out 2 | 3 | # cp from: https://github.com/yyle88/osexec/blob/5d35ce11e097573b53d03744479836fb7fdd7e85/Makefile#L4 4 | test: 5 | @if [ -d $(COVERAGE_DIR) ]; then rm -r $(COVERAGE_DIR); fi 6 | @mkdir $(COVERAGE_DIR) 7 | make test-with-flags TEST_FLAGS='-v -race -covermode atomic -coverprofile $$(COVERAGE_DIR)/combined.txt -bench=. -benchmem -timeout 20m' 8 | 9 | test-with-flags: 10 | @go test $(TEST_FLAGS) ./... 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /gormcnmstub/stub.go: -------------------------------------------------------------------------------- 1 | // Package gormcnmstub provides stub implementations and shared instances used in gormcnm operations 2 | // Auto exposes ColumnOperationClass instance with common operations like JOIN, COALESCE 3 | // Supports testing and development with pre-configured column operation patterns 4 | // 5 | // gormcnmstub 包提供 gormcnm 操作中使用的存根实现和共享实例 6 | // 自动暴露 ColumnOperationClass 实例,包含 JOIN、COALESCE 等常用操作 7 | // 支持使用预配置的列操作模式进行测试和开发 8 | package gormcnmstub 9 | 10 | import "github.com/yyle88/gormcnm" 11 | 12 | // stub provides a shared instance of ColumnOperationClass, intended to be used in testing 13 | // stub 提供 ColumnOperationClass 的共享实例,用于测试目的 14 | var stub = &gormcnm.ColumnOperationClass{} 15 | -------------------------------------------------------------------------------- /internal/tests/tests.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/yyle88/rese" 9 | "gorm.io/driver/sqlite" 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/logger" 12 | ) 13 | 14 | // NewDBRun runs a function with an in-memory SQLite database for testing purposes 15 | // Auto creates a temporary database connection and handles cleanup after function execution 16 | // NewDBRun 在内存数据库中运行函数,用于测试目的 17 | // 自动创建临时数据库连接并在函数执行后处理清理工作 18 | func NewDBRun(t *testing.T, run func(db *gorm.DB)) { 19 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 20 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 21 | Logger: logger.Default.LogMode(logger.Info), 22 | })) 23 | defer rese.F0(rese.P1(db.DB()).Close) 24 | 25 | t.Log("--- DB BEGIN ---") 26 | run(db) 27 | t.Log("--- DB CLOSE ---") 28 | } 29 | -------------------------------------------------------------------------------- /gorm_scope.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides GORM scope function type definitions, enabling custom GORM conditions 2 | // Auto enables type-safe scope functions that integrate with GORM's db.Scopes() method 3 | // Supports building reusable where modifiers and composable database operations 4 | // 5 | // gormcnm 提供 GORM 作用域函数类型定义,用于自定义查询条件 6 | // 自动启用类型安全的作用域函数,与 GORM 的 db.Scopes() 方法集成 7 | // 支持构建可重用的查询修饰符和可组合的数据库操作 8 | package gormcnm 9 | 10 | import ( 11 | "gorm.io/gorm" 12 | ) 13 | 14 | // ScopeFunction is a type alias, representing a function that modifies a GORM DB instance, 15 | // used with db.Scopes() to use custom GORM conditions. 16 | // See: https://github.com/go-gorm/gorm/blob/c44405a25b0fb15c20265e672b8632b8774793ca/chainable_api.go#L376 17 | // ScopeFunction 是用于修改 GORM DB 实例的函数类型别名, 18 | // 主要是和 db.Scopes() 配合使用,以应用自定义查询条件。 19 | // 详见:https://github.com/go-gorm/gorm/blob/c44405a25b0fb15c20265e672b8632b8774793ca/chainable_api.go#L376 20 | type ScopeFunction = func(db *gorm.DB) *gorm.DB 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 yangyile-yyle88 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /op_clause_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate GORM clause operations and column assignments 2 | // Auto verifies Clause, ClauseWithTable methods and ClauseColumn operations 3 | // Tests cover clause creation, table qualification, alias assignment, and GORM integration 4 | // 5 | // gormcnm 测试包验证 GORM 子句操作和列赋值 6 | // 自动验证 Clause、ClauseWithTable 方法和 ClauseColumn 功能 7 | // 测试涵盖子句创建、表限定、别名赋值和 GORM 集成 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/neatjson/neatjsons" 15 | ) 16 | 17 | func TestClauseColumn_Column(t *testing.T) { 18 | const columnName = ColumnName[string]("name") 19 | 20 | clause := columnName.Clause() 21 | column := clause.Column() 22 | t.Log(neatjsons.S(column)) 23 | require.Equal(t, columnName.Name(), column.Name) 24 | } 25 | 26 | func TestClauseColumn_Assignment(t *testing.T) { 27 | const columnRank = ColumnName[int]("rank") 28 | 29 | clauseType := columnRank.ClauseWithTable("students") 30 | assignment := clauseType.Assignment(888) 31 | t.Log(neatjsons.S(assignment)) 32 | require.Equal(t, assignment.Column.Name, columnRank.Name()) 33 | require.Equal(t, 888, assignment.Value) 34 | require.Equal(t, "students", assignment.Column.Table) 35 | } 36 | -------------------------------------------------------------------------------- /gormcnmstub/stub.gen_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnmstub tests validate code generation operations used in stub wrapper creation 2 | // Auto verifies generated code correctness and AST manipulation patterns 3 | // Tests cover stub generation, reflection-based code creation, and syntax validation 4 | // 5 | // gormcnmstub 测试包验证存根包装创建中使用的代码生成操作 6 | // 自动验证生成代码的正确性和 AST 操作模式 7 | // 测试涵盖存根生成、基于反射的代码创建和语法验证 8 | package gormcnmstub 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/yyle88/must" 14 | "github.com/yyle88/runpath" 15 | "github.com/yyle88/runpath/runtestpath" 16 | "github.com/yyle88/sure/cls_stub_gen" 17 | "github.com/yyle88/syntaxgo" 18 | "github.com/yyle88/syntaxgo/syntaxgo_ast" 19 | "github.com/yyle88/syntaxgo/syntaxgo_reflect" 20 | ) 21 | 22 | func TestGen(t *testing.T) { 23 | packageName := syntaxgo_reflect.GetPkgNameV4(stub) 24 | must.Equals("gormcnm", packageName) 25 | 26 | cfg := &cls_stub_gen.StubGenConfig{ 27 | SourceRootPath: runpath.PARENT.UpTo(2, packageName), 28 | TargetPackageName: syntaxgo.CurrentPackageName(), 29 | ImportOptions: syntaxgo_ast.NewPackageImportOptions(), 30 | OutputPath: runtestpath.SrcPath(t), 31 | AllowFileCreation: false, 32 | } 33 | 34 | param := cls_stub_gen.NewStubParam(stub, "stub") 35 | cls_stub_gen.GenerateStubs(cfg, param) 36 | } 37 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | // Package utils provides utility functions and helpers used across GORM column operations 2 | // Auto handles pointer operations, alias application, and test database setup 3 | // Supports testing with SQLite in-memory databases and column alias formatting 4 | // 5 | // utils 包提供跨 GORM 列操作使用的实用函数和辅助工具 6 | // 自动处理指针操作、别名应用和测试数据库设置 7 | // 支持使用 SQLite 内存数据库进行测试和列别名格式化 8 | package utils 9 | 10 | import ( 11 | "github.com/yyle88/tern" 12 | ) 13 | 14 | // GetValuePointer gets a pointer to any value, great to use with numbers 0,1,2,3 and strings "a", "b", "c" 15 | // GetValuePointer 给任何值取地址,特别是当参数为数字0,1,2,3或者字符串"a", "b", "c"的时候 16 | func GetValuePointer[T any](v T) *T { 17 | return &v 18 | } 19 | 20 | // GetPointerValue dereferences a pointer and returns the value, returns zero value if pointer is nil 21 | // GetPointerValue 给任何地址取值,当是空地址时返回 zero 即类型默认的零值 22 | func GetPointerValue[T any](v *T) T { 23 | if v != nil { 24 | return *v 25 | } 26 | return Zero[T]() 27 | } 28 | 29 | func Zero[T any]() T { 30 | var zero T 31 | return zero 32 | } 33 | 34 | // ApplyAliasToColumn applies an alias to a column statement, returns format like "COUNT(*) as cnt" 35 | // Auto appends alias if provided, otherwise returns original statement 36 | // ApplyAliasToColumn 为列语句设置别名,返回类似 "COUNT(*) as cnt" 的格式 37 | // 如果提供了别名就自动添加,否则返回原始语句 38 | func ApplyAliasToColumn(stmt string, alias string) string { 39 | return tern.BVV(alias != "", stmt+" as "+alias, stmt) 40 | } 41 | -------------------------------------------------------------------------------- /internal/examples/example1/example1_test.go: -------------------------------------------------------------------------------- 1 | package example1 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/require" 9 | "github.com/yyle88/done" 10 | "github.com/yyle88/gormcnm" 11 | "github.com/yyle88/gormcnm/internal/tests" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | func TestExample(t *testing.T) { 16 | // Example is a gorm model define 3 fields(name, type, rank) 17 | type Example struct { 18 | Name string `gorm:"primary_key;type:varchar(100);"` 19 | Type string `gorm:"column:type;"` 20 | Rank int `gorm:"column:rank;"` 21 | } 22 | 23 | // Now define the fields enum vars(name, type rank) 24 | const ( 25 | columnName = gormcnm.ColumnName[string]("name") 26 | columnType = gormcnm.ColumnName[string]("type") 27 | columnRank = gormcnm.ColumnName[int]("rank") 28 | ) 29 | 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | //create example data 32 | done.Done(db.AutoMigrate(&Example{})) 33 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 34 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 35 | 36 | { 37 | var res Example 38 | err := db.Where("name=?", "abc").First(&res).Error 39 | done.Done(err) 40 | fmt.Println(res) 41 | require.Equal(t, 123, res.Rank) 42 | } 43 | { //select an example data 44 | var res Example 45 | if err := db.Where(columnName.Eq("abc")). 46 | Where(columnType.Eq("xyz")). 47 | Where(columnRank.Gt(100)). 48 | Where(columnRank.Lt(200)). 49 | First(&res).Error; err != nil { 50 | panic(errors.WithMessage(err, "wrong")) 51 | } 52 | fmt.Println(res) 53 | require.Equal(t, 123, res.Rank) 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /internal/examples/example3/example3.go: -------------------------------------------------------------------------------- 1 | // Package example3 demonstrates namespace pattern and manual column definition with gormcnm 2 | // Auto shows User and Order models with Columns() methods returning column name collections 3 | // Supports avoiding column name conflicts across multiple tables 4 | // 5 | // example3 包演示使用 gormcnm 的命名空间模式和手动列定义 6 | // 自动展示 User 和 Order 模型,通过 Columns() 方法返回列名集合 7 | // 支持避免多个表之间的列名冲突 8 | package example3 9 | 10 | import "github.com/yyle88/gormcnm" 11 | 12 | // 当模型/表的数量比较多时,也可以使用名字空间把列名包裹起来 13 | // 比如下面的 UserColumns 和 OrderColumns 两个名字空间 14 | // 这样就能在不同表有重名的列时就能避免混淆 15 | // 区分得很清楚 16 | // 当然这个代码其实可以通过语法分析自动得到 17 | // 在项目 https://github.com/yyle88/gormcngen 里有自动生成列名的逻辑,就能自动配置啦,里面有各种示例,非常便捷 18 | 19 | type User struct { 20 | ID uint 21 | Name string 22 | } 23 | 24 | func (*User) TableName() string { 25 | return "users" 26 | } 27 | 28 | func (*User) Columns() *UserColumns { 29 | return &UserColumns{ 30 | ID: "id", 31 | Name: "name", 32 | } 33 | } 34 | 35 | type UserColumns struct { 36 | gormcnm.ColumnOperationClass //继承操作函数,让查询更便捷 37 | // 模型各个列名和类型: 38 | ID gormcnm.ColumnName[uint] 39 | Name gormcnm.ColumnName[string] 40 | } 41 | 42 | type Order struct { 43 | ID uint 44 | UserID uint 45 | Amount float64 46 | } 47 | 48 | func (*Order) TableName() string { 49 | return "orders" 50 | } 51 | 52 | func (*Order) Columns() *OrderColumns { 53 | return &OrderColumns{ 54 | ID: "id", 55 | UserID: "user_id", 56 | Amount: "amount", 57 | } 58 | } 59 | 60 | type OrderColumns struct { 61 | gormcnm.ColumnOperationClass //继承操作函数,让查询更便捷 62 | // 模型各个列名和类型: 63 | ID gormcnm.ColumnName[uint] 64 | UserID gormcnm.ColumnName[uint] 65 | Amount gormcnm.ColumnName[float64] 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yyle88/gormcnm 2 | 3 | go 1.22.8 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/pkg/errors v0.9.1 8 | github.com/stretchr/testify v1.11.1 9 | github.com/yyle88/done v1.0.28 10 | github.com/yyle88/must v0.0.28 11 | github.com/yyle88/neatjson v0.0.13 12 | github.com/yyle88/rese v0.0.12 13 | github.com/yyle88/runpath v1.0.25 14 | github.com/yyle88/sure v0.0.42 15 | github.com/yyle88/syntaxgo v0.0.53 16 | github.com/yyle88/tern v0.0.10 17 | gorm.io/datatypes v1.2.7 18 | gorm.io/driver/sqlite v1.6.0 19 | gorm.io/gorm v1.31.1 20 | ) 21 | 22 | require ( 23 | filippo.io/edwards25519 v1.1.0 // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/go-sql-driver/mysql v1.9.3 // indirect 26 | github.com/jinzhu/inflection v1.0.0 // indirect 27 | github.com/jinzhu/now v1.1.5 // indirect 28 | github.com/kr/pretty v0.3.1 // indirect 29 | github.com/kr/text v0.2.0 // indirect 30 | github.com/mattn/go-sqlite3 v1.14.32 // indirect 31 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 32 | github.com/rogpeppe/go-internal v1.13.1 // indirect 33 | github.com/yyle88/erero v1.0.24 // indirect 34 | github.com/yyle88/formatgo v1.0.28 // indirect 35 | github.com/yyle88/mutexmap v1.0.15 // indirect 36 | github.com/yyle88/printgo v1.0.6 // indirect 37 | github.com/yyle88/zaplog v0.0.27 // indirect 38 | go.uber.org/multierr v1.11.0 // indirect 39 | go.uber.org/zap v1.27.1 // indirect 40 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect 41 | golang.org/x/mod v0.23.0 // indirect 42 | golang.org/x/sync v0.11.0 // indirect 43 | golang.org/x/text v0.22.0 // indirect 44 | golang.org/x/tools v0.30.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | gorm.io/driver/mysql v1.6.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /select_statement_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate SELECT statement building and column selection 2 | // Auto verifies SelectStatement features with GORM Select clauses 3 | // Tests examine column selection, aggregation functions, and SQL execution 4 | // 5 | // gormcnm 测试包验证 SELECT 语句构建和列选择 6 | // 自动验证 SelectStatement 功能与 GORM Select 子句 7 | // 测试涵盖列选择、聚合函数和查询执行 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestSelectStatement_Combine(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Rank int `gorm:"column:rank;"` 23 | } 24 | 25 | const ( 26 | columnName = ColumnName[string]("name") 27 | columnRank = ColumnName[int]("rank") 28 | ) 29 | 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | require.NoError(t, db.AutoMigrate(&Example{})) 32 | require.NoError(t, db.Save(&Example{Name: "abc", Rank: 100}).Error) 33 | require.NoError(t, db.Save(&Example{Name: "aaa", Rank: 101}).Error) 34 | 35 | type Result struct { 36 | Name string 37 | Mark int 38 | } 39 | 40 | selectStatement := NewSelectStatement(columnName.Name()).Combine(NewSx(columnRank.AsAlias("mark"))) 41 | 42 | var results []*Result 43 | //这是因为 rank 是您查询中的实际字段,而 mark 是 rank 字段的别名。在 ORDER BY 子句中,您应该使用实际的列名(即 rank),而不是别名(即 mark)。 44 | require.NoError(t, db.Model(&Example{}).Select(selectStatement.Qx0()).Order(columnRank.Ob("desc").Ox()).Find(&results).Error) 45 | t.Log(neatjsons.S(results)) 46 | 47 | require.Len(t, results, 2) 48 | require.Equal(t, results[0].Name, "aaa") 49 | require.Equal(t, results[0].Mark, 101) 50 | require.Equal(t, results[1].Name, "abc") 51 | require.Equal(t, results[1].Mark, 100) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /orders_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate ordering operations and sort statement construction 2 | // Auto verifies OrderByBottle features with GORM Order clauses 3 | // Tests examine basic ordering, combined sorting, and SQL execution 4 | // 5 | // gormcnm 测试包验证排序操作和排序语句构建 6 | // 自动验证 OrderByBottle 功能与 GORM Order 子句 7 | // 测试涵盖基础排序、组合排序和查询执行 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestOrderByBottle_Ob(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Type string `gorm:"column:type;"` 23 | } 24 | 25 | const ( 26 | columnName = ColumnName[string]("name") 27 | columnType = ColumnName[string]("type") 28 | ) 29 | 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | require.NoError(t, db.AutoMigrate(&Example{})) 32 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 33 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 34 | 35 | { 36 | var res []*Example 37 | require.NoError(t, db.Where(columnName.In([]string{"abc", "aaa"})). 38 | Order(columnName.OrderByBottle("asc"). 39 | Ob(columnType.Ob("desc")). 40 | Ox()). 41 | Find(&res).Error) 42 | require.Equal(t, "aaa", res[0].Name) 43 | require.Equal(t, "abc", res[1].Name) 44 | t.Log(neatjsons.S(res)) 45 | } 46 | { 47 | var res []*Example 48 | require.NoError(t, db.Where(columnName.In([]string{"abc", "aaa"})). 49 | Order(columnName.Ob("desc"). 50 | OrderByBottle(columnType.Ob("asc")). 51 | Orders()). 52 | Find(&res).Error) 53 | require.Equal(t, "abc", res[0].Name) 54 | require.Equal(t, "aaa", res[1].Name) 55 | t.Log(neatjsons.S(res)) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /internal/utils/types.go: -------------------------------------------------------------------------------- 1 | // Package utils provides utility types and interfaces for GORM table operations 2 | // Auto discover and handle column names with type safety and interface abstractions 3 | // Support both direct table name strings and GORM table interface implementations 4 | // 5 | // utils 包提供 GORM 表操作的实用类型和接口 6 | // 自动发现和处理列名,具有类型安全性和接口抽象 7 | // 支持直接表名字符串和 GORM 表接口实现 8 | package utils 9 | 10 | // ColumnNameInterface defines the interface for column name providers 11 | // ColumnNameInterface 定义列名提供者的接口 12 | type ColumnNameInterface interface { 13 | Name() string 14 | } 15 | 16 | // GormTableNameFace defines the interface for GORM table name providers 17 | // Compatible with GORM's table name interface pattern for seamless integration 18 | // Used in table join operations and qualified column name generation 19 | // 20 | // GormTableNameFace 定义 GORM 表名提供者的接口 21 | // 兼容 GORM 表名接口模式以实现无缝集成 22 | // 用于表连接操作和限定列名生成 23 | type GormTableNameFace interface { 24 | TableName() string // Returns the table name for database operations // 返回用于数据库操作的表名 25 | } 26 | 27 | // TableNameImp provides a simple implementation of table name interface 28 | // Acts as a concrete implementation when direct table name string is available 29 | // Used internally for table operations that need GormTableNameFace compliance 30 | // 31 | // TableNameImp 提供表名接口的简单实现 32 | // 在有直接表名字符串时作为具体实现 33 | // 内部用于需要符合 GormTableNameFace 的表操作 34 | type TableNameImp struct { 35 | tableName string // Internal storage for table name // 表名的内部存储 36 | } 37 | 38 | // NewTableNameImp creates a new TableNameImp instance with the specified table name 39 | // NewTableNameImp 使用指定的表名创建一个新的 TableNameImp 实例 40 | func NewTableNameImp(tableName string) *TableNameImp { 41 | return &TableNameImp{tableName: tableName} 42 | } 43 | 44 | // TableName returns the table name 45 | // TableName 返回表名 46 | func (X *TableNameImp) TableName() string { 47 | return X.tableName 48 | } 49 | -------------------------------------------------------------------------------- /internal/examples/example2/example2_test.go: -------------------------------------------------------------------------------- 1 | package example2 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/require" 9 | "github.com/yyle88/done" 10 | "github.com/yyle88/gormcnm" 11 | "github.com/yyle88/gormcnm/internal/tests" 12 | "github.com/yyle88/gormcnm/internal/utils" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func TestExample(t *testing.T) { 17 | // Example is a gorm model define 3 fields(name, type, rank) 18 | type Example struct { 19 | Name string `gorm:"primary_key;type:varchar(100);"` 20 | Type *string `gorm:"column:type;"` //通常不建议把字段定义为指针类型,但不建议不表示不能,因此要试试这种场景 21 | Rank *int `gorm:"column:rank;"` //通常不建议把字段定义为指针类型,但不建议不表示不能,因此要试试这种场景 22 | } 23 | 24 | // Now define the fields enum vars(name, type rank) 25 | const ( 26 | columnName = gormcnm.ColumnName[string]("name") 27 | columnType = gormcnm.ColumnName[*string]("type") 28 | columnRank = gormcnm.ColumnName[*int]("rank") 29 | ) 30 | 31 | tests.NewDBRun(t, func(db *gorm.DB) { 32 | //create example data 33 | done.Done(db.AutoMigrate(&Example{})) 34 | done.Done(db.Save(&Example{Name: "abc", Type: utils.GetValuePointer("xyz"), Rank: utils.GetValuePointer(123)}).Error) 35 | done.Done(db.Save(&Example{Name: "aaa", Type: utils.GetValuePointer("xxx"), Rank: utils.GetValuePointer(456)}).Error) 36 | 37 | { 38 | var res Example 39 | err := db.Where("name=?", "abc").First(&res).Error 40 | done.Done(err) 41 | fmt.Println(res) 42 | require.Equal(t, 123, utils.GetPointerValue(res.Rank)) 43 | } 44 | { //select an example data 45 | var res Example 46 | if err := db.Where(columnName.Eq("abc")). 47 | Where(columnType.Eq(utils.GetValuePointer("xyz"))). 48 | Where(columnRank.Gt(utils.GetValuePointer(100))). 49 | Where(columnRank.Lt(utils.GetValuePointer(200))). 50 | First(&res).Error; err != nil { 51 | panic(errors.WithMessage(err, "wrong")) 52 | } 53 | fmt.Println(res) 54 | require.Equal(t, 123, utils.GetPointerValue(res.Rank)) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /value_map_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate column value map operations for updates 2 | // Auto verifies ColumnValueMap functionality with GORM Updates and Save operations 3 | // Tests cover key-value mapping, batch updates, and value management 4 | // 5 | // gormcnm 测试包验证用于更新的列值映射操作 6 | // 自动验证 ColumnValueMap 功能与 GORM Updates 和 Save 操作 7 | // 测试涵盖键值映射、批量更新和值管理 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | func TestValuesMap_SetValue(t *testing.T) { 19 | type Example struct { 20 | Name string `gorm:"primary_key;type:varchar(100);"` 21 | Type string `gorm:"column:type;"` 22 | Rank int `gorm:"column:rank;"` 23 | } 24 | 25 | const ( 26 | columnName = ColumnName[string]("name") 27 | columnType = ColumnName[string]("type") 28 | columnRank = ColumnName[int]("rank") 29 | ) 30 | 31 | tests.NewDBRun(t, func(db *gorm.DB) { 32 | require.NoError(t, db.AutoMigrate(&Example{})) 33 | require.NoError(t, db.Save(&Example{ 34 | Name: "aaa", 35 | Type: "xxx", 36 | Rank: 123, 37 | }).Error) 38 | require.NoError(t, db.Save(&Example{ 39 | Name: "bbb", 40 | Type: "yyy", 41 | Rank: 456, 42 | }).Error) 43 | 44 | { 45 | result := db.Model(&Example{}).Where( 46 | Qx(columnName.Eq("aaa")). 47 | AND( 48 | Qx(columnType.Eq("xxx")), 49 | Qx(columnRank.Eq(123)), 50 | ).Qx3(), 51 | ).UpdateColumns(columnRank.Kw(100).Kw(columnType.Kv("zzz")).Kws()) 52 | require.NoError(t, result.Error) 53 | require.Equal(t, int64(1), result.RowsAffected) 54 | } 55 | { 56 | result := db.Model(&Example{}).Where( 57 | Qx( 58 | columnName.Eq("bbb"), 59 | ).AND1( 60 | columnType.Eq("yyy"), 61 | ).AND1( 62 | columnRank.Eq(456), 63 | ).Qx3(), 64 | ).UpdateColumns(columnRank.Kw(200).Kw(columnType.Kv("www")).Map()) 65 | require.NoError(t, result.Error) 66 | require.Equal(t, int64(1), result.RowsAffected) 67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /statement_args_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate statement and arguments tuple operations 2 | // Auto verifies statementArgumentsTuple features with SQL statement and argument management 3 | // Tests examine driver.Valuer implementation, argument binding, and GORM queries integration 4 | // 5 | // gormcnm 测试包验证语句和参数元组操作 6 | // 自动验证 statementArgumentsTuple 功能,包含 SQL 语句和参数管理 7 | // 测试涵盖 driver.Valuer 实现、参数绑定和 GORM 查询集成 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | func Test_statementArgumentsTuple_Qx1(t *testing.T) { 19 | oneItem := newStatementArgumentsTuple("name=?", []any{"abc"}) 20 | stmt, arg0 := oneItem.Qx1() 21 | require.Equal(t, "name=?", stmt) 22 | require.Equal(t, "abc", arg0) 23 | } 24 | 25 | func Test_statementArgumentsTuple_Qx2(t *testing.T) { 26 | type Example struct { 27 | Name string `gorm:"primary_key;type:varchar(100);"` 28 | Rank int `gorm:"column:rank;"` 29 | } 30 | 31 | const ( 32 | columnName = ColumnName[string]("name") 33 | columnRank = ColumnName[int]("rank") 34 | ) 35 | 36 | tests.NewDBRun(t, func(db *gorm.DB) { 37 | require.NoError(t, db.AutoMigrate(&Example{})) 38 | require.NoError(t, db.Save(&Example{Name: "abc", Rank: 100}).Error) 39 | require.NoError(t, db.Save(&Example{Name: "aaa", Rank: 101}).Error) 40 | 41 | stmtItem := newStatementArgumentsTuple(columnName.Name()+"=?"+" AND "+columnRank.Name()+"=?", []any{"abc", 100}) 42 | stmt, arg1, arg2 := stmtItem.Qx2() 43 | require.Equal(t, "name=? AND rank=?", stmt) 44 | require.Equal(t, "abc", arg1) 45 | require.Equal(t, 100, arg2) 46 | 47 | var count int64 48 | require.NoError(t, db.Model(&Example{}).Where(stmtItem.Qx2()).Count(&count).Error) 49 | require.Equal(t, int64(1), count) 50 | 51 | var example Example 52 | require.NoError(t, db.Where(stmtItem.Qx2()).First(&example).Error) 53 | require.Equal(t, "abc", example.Name) 54 | require.Equal(t, 100, example.Rank) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /internal/examples/example4/example4.go: -------------------------------------------------------------------------------- 1 | // Package example4 demonstrates decoration pattern and dynamic column generation with gormcnm 2 | // Auto shows User and Order models with TableColumns() methods using decoration pattern 3 | // Supports dynamic column name prefixing and flexible column name transformations 4 | // 5 | // example4 包演示使用 gormcnm 的装饰模式和动态列生成 6 | // 自动展示 User 和 Order 模型,通过 TableColumns() 方法使用装饰模式 7 | // 支持动态列名前缀和灵活的列名转换 8 | package example4 9 | 10 | import "github.com/yyle88/gormcnm" 11 | 12 | // 当模型/表的数量比较多时,也可以使用名字空间把列名包裹起来 13 | // 比如下面的 UserColumns 和 OrderColumns 两个名字空间 14 | // 这样就能在不同表有重名的列时就能避免混淆 15 | // 区分得很清楚 16 | // 当然这个代码其实可以通过语法分析自动得到 17 | // 在项目 https://github.com/yyle88/gormcngen 里有自动生成列名的逻辑,就能自动配置啦,里面有各种示例,非常便捷 18 | 19 | type User struct { 20 | ID uint 21 | Name string 22 | } 23 | 24 | func (*User) TableName() string { 25 | return "users" 26 | } 27 | 28 | func (T *User) Columns() *UserColumns { 29 | return T.TableColumns(gormcnm.NewPlainDecoration()) 30 | } 31 | 32 | func (T *User) TableColumns(decoration gormcnm.ColumnNameDecoration) *UserColumns { 33 | return &UserColumns{ 34 | ID: gormcnm.Cmn(T.ID, "id", decoration), 35 | Name: gormcnm.Cmn(T.Name, "name", decoration), 36 | } 37 | } 38 | 39 | type UserColumns struct { 40 | gormcnm.ColumnOperationClass //继承操作函数,让查询更便捷 41 | // 模型各个列名和类型: 42 | ID gormcnm.ColumnName[uint] 43 | Name gormcnm.ColumnName[string] 44 | } 45 | 46 | type Order struct { 47 | ID uint 48 | UserID uint 49 | Amount float64 50 | } 51 | 52 | func (*Order) TableName() string { 53 | return "orders" 54 | } 55 | 56 | func (T *Order) Columns() *OrderColumns { 57 | return T.TableColumns(gormcnm.NewPlainDecoration()) 58 | } 59 | 60 | func (T *Order) TableColumns(decoration gormcnm.ColumnNameDecoration) *OrderColumns { 61 | return &OrderColumns{ 62 | ID: gormcnm.Cmn(T.ID, "id", decoration), 63 | UserID: gormcnm.Cmn(T.UserID, "user_id", decoration), 64 | Amount: gormcnm.Cmn(T.Amount, "amount", decoration), 65 | } 66 | } 67 | 68 | type OrderColumns struct { 69 | gormcnm.ColumnOperationClass //继承操作函数,让查询更便捷 70 | // 模型各个列名和类型: 71 | ID gormcnm.ColumnName[uint] 72 | UserID gormcnm.ColumnName[uint] 73 | Amount gormcnm.ColumnName[float64] 74 | } 75 | -------------------------------------------------------------------------------- /internal/demos/demo2x/main.go: -------------------------------------------------------------------------------- 1 | // Package main demonstrates advanced gormcnm usage with multi-condition WHERE queries 2 | // Auto shows comparison with traditional GORM queries vs type-safe column operations 3 | // Runs with SQLite in-memory database to showcase Gt, Lt, Eq operations 4 | // 5 | // main 包演示 gormcnm 多条件 WHERE 查询的高级用法 6 | // 自动展示传统 GORM 查询与类型安全列操作的对比 7 | // 使用 SQLite 内存数据库运行以展示 Gt、Lt、Eq 操作 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/google/uuid" 14 | "github.com/yyle88/done" 15 | "github.com/yyle88/gormcnm" 16 | "github.com/yyle88/must" 17 | "github.com/yyle88/rese" 18 | "gorm.io/driver/sqlite" 19 | "gorm.io/gorm" 20 | "gorm.io/gorm/logger" 21 | ) 22 | 23 | // Example is a gorm model define 3 fields(name, type, rank) 24 | type Example struct { 25 | Name string `gorm:"primary_key;type:varchar(100);"` 26 | Type string `gorm:"column:type;"` 27 | Rank int `gorm:"column:rank;"` 28 | } 29 | 30 | // Now define the fields enum vars(name, type rank) 31 | const ( 32 | columnName = gormcnm.ColumnName[string]("name") 33 | columnType = gormcnm.ColumnName[string]("type") 34 | columnRank = gormcnm.ColumnName[int]("rank") 35 | ) 36 | 37 | func main() { 38 | //new db connection 39 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 40 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 41 | Logger: logger.Default.LogMode(logger.Info), 42 | })) 43 | defer rese.F0(rese.P1(db.DB()).Close) 44 | 45 | //create example data 46 | done.Done(db.AutoMigrate(&Example{})) 47 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 48 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 49 | 50 | { 51 | //SELECT * FROM `examples` WHERE name="abc" ORDER BY `examples`.`name` LIMIT 1 52 | var res Example 53 | must.Done(db.Where("name=?", "abc").First(&res).Error) 54 | fmt.Println(res) 55 | } 56 | { 57 | //SELECT * FROM `examples` WHERE name="abc" AND type="xyz" AND rank>100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 58 | var res Example 59 | must.Done(db.Where(columnName.Eq("abc")). 60 | Where(columnType.Eq("xyz")). 61 | Where(columnRank.Gt(100)). 62 | Where(columnRank.Lt(200)). 63 | First(&res).Error) 64 | fmt.Println(res) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /qs_coalesce_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate COALESCE and IFNULL operations for NULL-safe aggregates 2 | // Auto verifies CoalesceNonNullGuardian functionality with SUM, AVG, MAX, MIN operations 3 | // Tests cover NULL value protection, default value handling, and MySQL/standard SQL compatibility 4 | // 5 | // gormcnm 测试包验证 COALESCE 和 IFNULL 操作,实现 NULL 安全的聚合函数 6 | // 自动验证 CoalesceNonNullGuardian 功能,包含 SUM、AVG、MAX、MIN 操作 7 | // 测试涵盖 NULL 值保护、默认值处理和 MySQL/标准 SQL 兼容性 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "gorm.io/gorm" 16 | ) 17 | 18 | func TestCoalesceStmt(t *testing.T) { 19 | type Example struct { 20 | Name string `gorm:"primary_key;type:varchar(100);"` 21 | Rank int `gorm:"column:rank;"` 22 | } 23 | 24 | const columnRank = ColumnName[int]("rank") 25 | 26 | tests.NewDBRun(t, func(db *gorm.DB) { 27 | require.NoError(t, db.AutoMigrate(&Example{})) 28 | require.NoError(t, db.Save(&Example{ 29 | Name: "aaa", 30 | Rank: 123, 31 | }).Error) 32 | require.NoError(t, db.Save(&Example{ 33 | Name: "bbb", 34 | Rank: 456, 35 | }).Error) 36 | 37 | { 38 | var value int 39 | err := db.Model(&Example{}).Select(columnRank.IFNULLFN().MaxStmt("max_rank")).First(&value).Error 40 | require.NoError(t, err) 41 | require.Equal(t, 456, value) 42 | } 43 | 44 | { 45 | var value int 46 | err := db.Model(&Example{}).Select(columnRank.COALESCE().MaxStmt("")).First(&value).Error 47 | require.NoError(t, err) 48 | require.Equal(t, 456, value) 49 | } 50 | { 51 | type resType struct { 52 | Value int 53 | } 54 | var res resType 55 | err := db.Model(&Example{}).Select(columnRank.COALESCE().MinStmt("value")).First(&res).Error 56 | require.NoError(t, err) 57 | require.Equal(t, 123, res.Value) 58 | } 59 | { 60 | type resType struct { 61 | Value int 62 | } 63 | var res resType 64 | err := db.Model(&Example{}).Select(columnRank.COALESCE().SumStmt("value")).First(&res).Error 65 | require.NoError(t, err) 66 | require.Equal(t, 579, res.Value) 67 | } 68 | { 69 | var value float64 70 | err := db.Model(&Example{}).Select(columnRank.COALESCE().AvgStmt("alias")).First(&value).Error 71 | require.NoError(t, err) 72 | require.Equal(t, 289.5, value) 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /new_column_name_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate column name creation and decoration operations 2 | // Auto verifies New, Cnm, Cmn constructors with PlainDecoration, TableDecoration, CustomDecoration 3 | // Tests cover column name instantiation, table prefix decoration, and custom transformation logic 4 | // 5 | // gormcnm 测试包验证列名创建和装饰操作 6 | // 自动验证 New、Cnm、Cmn 构造函数,包含 PlainDecoration、TableDecoration、CustomDecoration 7 | // 测试涵盖列名实例化、表前缀装饰和自定义转换逻辑 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestNew(t *testing.T) { 17 | columnName := New[string]("name") 18 | 19 | qs, value := columnName.Eq("abc") 20 | t.Log(qs, value) 21 | 22 | require.Equal(t, "name=?", qs) 23 | require.Equal(t, "abc", value) 24 | } 25 | 26 | func TestCnm(t *testing.T) { 27 | type Example struct { 28 | Name string `gorm:"column:name;primary_key;type:varchar(100);"` 29 | } 30 | 31 | res := Example{} 32 | columnName := Cnm(res.Name, "name") 33 | 34 | qs, value := columnName.Eq("abc") 35 | t.Log(qs, value) 36 | 37 | require.Equal(t, "name=?", qs) 38 | require.Equal(t, "abc", value) 39 | } 40 | 41 | func TestCmn(t *testing.T) { 42 | type Example struct { 43 | Name string `gorm:"column:name;primary_key;type:varchar(100);"` 44 | } 45 | 46 | decoration := NewPlainDecoration() 47 | 48 | res := Example{} 49 | columnName := Cmn(res.Name, "name", decoration) 50 | 51 | qs, value := columnName.Eq("abc") 52 | t.Log(qs, value) 53 | 54 | require.Equal(t, "name=?", qs) 55 | require.Equal(t, "abc", value) 56 | } 57 | 58 | func TestCmn_WithTableNameDecoration(t *testing.T) { 59 | type Example struct { 60 | Name string `gorm:"column:name;primary_key;type:varchar(100);"` 61 | } 62 | 63 | decoration := NewTableDecoration("examples") 64 | 65 | res := Example{} 66 | columnName := Cmn(res.Name, "name", decoration) 67 | 68 | qs, value := columnName.Eq("abc") 69 | t.Log(qs, value) 70 | 71 | require.Equal(t, "examples.name=?", qs) 72 | require.Equal(t, "abc", value) 73 | } 74 | 75 | func TestCmn_WithCustomDecoration(t *testing.T) { 76 | type Example struct { 77 | Name string `gorm:"column:name;primary_key;type:varchar(100);"` 78 | } 79 | 80 | decoration := NewCustomDecoration(func(name string) string { 81 | return "examples" + "." + name 82 | }) 83 | 84 | res := Example{} 85 | columnName := Cmn(res.Name, "name", decoration) 86 | 87 | qs, value := columnName.Eq("abc") 88 | t.Log(qs, value) 89 | 90 | require.Equal(t, "examples.name=?", qs) 91 | require.Equal(t, "abc", value) 92 | } 93 | -------------------------------------------------------------------------------- /orders.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides ORDER BY statement building operations with flexible sorting patterns 2 | // Auto creates OrderByBottle instances with ASC/DESC direction support and combination logic 3 | // Supports building complex sorting clauses with multiple columns and GORM integration 4 | // 5 | // gormcnm 包提供 ORDER BY 语句构建操作,具有灵活的排序模式 6 | // 自动创建 OrderByBottle 实例,支持 ASC/DESC 方向和组合逻辑 7 | // 支持构建多列的复杂排序子句,具有 GORM 集成 8 | package gormcnm 9 | 10 | import "gorm.io/gorm" 11 | 12 | // OrderByBottle represents a sort statement construction toolkit, designed with a unique naming style that reflects a materials science focus. 13 | // OrderByBottle 代表排序语句构建器,使用了与材料学相关的命名风格。 14 | type OrderByBottle string 15 | 16 | // Ob concatenates the current OrderByBottle with the next one, forming a combined ordering string. 17 | // Ob 将当前的 OrderByBottle 与下一个 OrderByBottle 连接,形成一个组合的排序字符串。 18 | func (ob OrderByBottle) Ob(next OrderByBottle) OrderByBottle { 19 | return ob + " , " + next 20 | } 21 | 22 | // OrderByBottle concatenates the current OrderByBottle with the next one, forming a combined ordering string. 23 | // OrderByBottle 将当前的 OrderByBottle 与下一个 OrderByBottle 连接,形成一个组合的排序字符串。 24 | func (ob OrderByBottle) OrderByBottle(next OrderByBottle) OrderByBottle { 25 | return ob + " , " + next 26 | } 27 | 28 | // Ox converts the OrderByBottle to a string. Note that if the type is not specific, it could be ignored by GORM's logic. 29 | // Ox 将 OrderByBottle 转换为字符串。请注意,如果类型不明确,它可能会被 GORM 的逻辑忽略。 30 | // This is an unavoidable limitation due to GORM's handling of the Order field logic. 31 | // 这是由于 GORM 对 Order 字段逻辑的处理所造成的无法避免的限制。 32 | // Developers might forget to convert this to a string before passing it to GORM, so it is important to do this step. 33 | // 开发者可能会忘记在传递给 GORM 之前将其转换为字符串,因此需要记住这一点。 34 | // There is no elegant solution to this limitation now, but it should work fine in most cases. 35 | // 现在没有优雅的解决方案,但在大多数情况下应该没有问题。 36 | func (ob OrderByBottle) Ox() string { 37 | return string(ob) 38 | } 39 | 40 | // Orders converts the OrderByBottle to a string. Note that if the type is not specific, it could be ignored by GORM's logic. 41 | // Orders 将 OrderByBottle 转换为字符串。请注意,如果类型不明确,它可能会被 GORM 的逻辑忽略。 42 | func (ob OrderByBottle) Orders() string { 43 | return string(ob) 44 | } 45 | 46 | // Scope converts the OrderByBottle to a GORM ScopeFunction used with db.Scopes(). 47 | // It applies the ordering defined by OrderByBottle to the GORM select. 48 | // Scope 将 OrderByBottle 转换为 GORM 的 ScopeFunction,以便被 db.Scopes() 调用。 49 | // 它将 OrderByBottle 定义的排序规则应用于 GORM 查询。 50 | func (ob OrderByBottle) Scope() ScopeFunction { 51 | return func(db *gorm.DB) *gorm.DB { 52 | return db.Order(string(ob)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /table_tbjoin.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides table JOIN operations to build multi-table data relationships 2 | // Auto creates LEFT JOIN, INNER JOIN, and custom JOIN operations with ON conditions 3 | // Supports building complex table relationships with type-safe join clause construction 4 | // 5 | // gormcnm 提供表 JOIN 操作,用于构建多表查询关系 6 | // 自动创建 LEFT JOIN、INNER JOIN 和自定义 JOIN 操作,包含 ON 条件 7 | // 支持构建复杂的表关系,具备类型安全的连接子句构建 8 | package gormcnm 9 | 10 | import ( 11 | "strings" 12 | 13 | "gorm.io/gorm/clause" 14 | ) 15 | 16 | // LEFTJOIN creates a left join operation on the specified table. 17 | // LEFTJOIN 给指定表创建一个左连接操作。 18 | func (common *ColumnOperationClass) LEFTJOIN(tableName string) *TableJoin { 19 | return newTableJoin(clause.LeftJoin, tableName) 20 | } 21 | 22 | // RIGHTJOIN creates a right join operation on the specified table. 23 | // RIGHTJOIN 给指定表创建一个右连接操作。 24 | func (common *ColumnOperationClass) RIGHTJOIN(tableName string) *TableJoin { 25 | return newTableJoin(clause.RightJoin, tableName) 26 | } 27 | 28 | // INNERJOIN creates an INNER join operation on the specified table. 29 | // INNERJOIN 给指定表创建一个内连接操作。 30 | func (common *ColumnOperationClass) INNERJOIN(tableName string) *TableJoin { 31 | return newTableJoin(clause.InnerJoin, tableName) 32 | } 33 | 34 | // CROSSJOIN creates a cross join operation on the specified table. 35 | // CROSSJOIN 给指定表创建一个交叉连接操作。 36 | func (common *ColumnOperationClass) CROSSJOIN(tableName string) *TableJoin { 37 | return newTableJoin(clause.CrossJoin, tableName) 38 | } 39 | 40 | // TableJoin represents a join operation on a table, including its type and name 41 | // Supports every standard SQL join type: LEFT, RIGHT, INNER, and CROSS joins 42 | // Auto generates correct SQL JOIN syntax with ON clause conditions 43 | // 44 | // TableJoin 表示表上的连接操作,包括连接类型和表名 45 | // 支持所有标准 SQL 连接类型:LEFT、RIGHT、INNER 和 CROSS 连接 46 | // 自动生成适当的 SQL JOIN 语法和 ON 子句条件 47 | type TableJoin struct { 48 | whichJoin clause.JoinType // Type of join (LEFT, RIGHT, INNER, CROSS) // 连接类型(LEFT、RIGHT、INNER、CROSS) 49 | tableName string // Name of the table involved in the join // 参与连接的表名 50 | } 51 | 52 | // newTableJoin creates a new TableJoin instance with the specified table name and join type. 53 | // newTableJoin 使用指定的表名和连接类型创建一个新的 TableJoin 实例。 54 | func newTableJoin(whichJoin clause.JoinType, tableName string) *TableJoin { 55 | return &TableJoin{ 56 | whichJoin: whichJoin, 57 | tableName: tableName, 58 | } 59 | } 60 | 61 | // On generates the SQL ON clause for the join, combining multiple statements with "AND". 62 | // On 给连接生成 SQL 的 ON 子句,将多个语句用 "AND" 组合。 63 | func (op *TableJoin) On(stmts ...string) string { 64 | return string(op.whichJoin) + " JOIN " + op.tableName + " ON " + strings.Join(stmts, " AND ") 65 | } 66 | -------------------------------------------------------------------------------- /qs_conjunction_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate conjunction operations for complex WHERE clauses 2 | // Auto verifies QsConjunction functionality with AND, OR, NOT logical operators 3 | // Tests cover basic conjunctions, nested conditions, and SQL statement composition 4 | // 5 | // gormcnm 测试包验证查询语句连接词操作,用于复杂的 WHERE 子句 6 | // 自动验证 QsConjunction 功能,包含 AND、OR、NOT 逻辑运算符 7 | // 测试涵盖基础连接词、嵌套条件和 SQL 语句组合 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestQsConjunction_AND(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Type string `gorm:"column:type;"` 23 | } 24 | 25 | const ( 26 | columnName = ColumnName[string]("name") 27 | columnType = ColumnName[string]("type") 28 | ) 29 | 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | require.NoError(t, db.AutoMigrate(&Example{})) 32 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 33 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 34 | 35 | { 36 | var one Example 37 | require.NoError(t, db.Where(columnName.Qc("=?").AND(columnType.Qc("=?")).Qs(), "abc", "xyz").First(&one).Error) 38 | require.Equal(t, "abc", one.Name) 39 | t.Log(neatjsons.S(one)) 40 | } 41 | { 42 | var res []*Example 43 | require.NoError(t, db.Where( 44 | columnName.Qc("=?"). 45 | OR( 46 | columnName.Qc("=?"), 47 | columnName.Qc("=?"), 48 | ). 49 | AND( 50 | columnName.Qc("IS NOT NULL"), 51 | columnType.Qc("IS NOT NULL"), 52 | ).Qs(), "abc", "aaa", "bbb").Find(&res).Error) 53 | require.Contains(t, []string{"abc", "aaa"}, res[0].Name) 54 | require.Contains(t, []string{"abc", "aaa"}, res[1].Name) 55 | t.Log(neatjsons.S(res)) 56 | } 57 | { 58 | var one Example 59 | require.NoError(t, db.Where(columnName.Qc("=?").NOT().Qs(), "abc").First(&one).Error) 60 | require.NotEqual(t, "abc", one.Name) 61 | t.Log(neatjsons.S(one)) 62 | } 63 | }) 64 | } 65 | 66 | func TestQsConjunction_AND_2(t *testing.T) { 67 | type Example struct { 68 | Name string `gorm:"primary_key;type:varchar(100);"` 69 | Type string `gorm:"column:type;"` 70 | } 71 | 72 | const ( 73 | columnName = ColumnName[string]("name") 74 | columnType = ColumnName[string]("type") 75 | ) 76 | 77 | tests.NewDBRun(t, func(db *gorm.DB) { 78 | require.NoError(t, db.AutoMigrate(&Example{})) 79 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 80 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 81 | 82 | { 83 | var one Example 84 | require.NoError(t, db.Where(columnName.Qc("=?").AND(columnType.Qc("=?")).Qs(), "abc", "xyz").First(&one).Error) 85 | require.Equal(t, "abc", one.Name) 86 | t.Log(neatjsons.S(one)) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /op_clause.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides GORM clause operations for type-safe column assignments 2 | // Auto creates clause.Column and clause.Assignment instances with table and alias support 3 | // Supports building complex SQL clauses with proper column qualification and raw SQL handling 4 | // 5 | // gormcnm 提供 GORM 子句操作,实现类型安全的列赋值 6 | // 自动创建 clause.Column 和 clause.Assignment 实例,支持表名和别名 7 | // 支持构建复杂的 SQL 子句,具备正确的列限定和原始 SQL 处理 8 | package gormcnm 9 | 10 | import "gorm.io/gorm/clause" 11 | 12 | // Clause creates a ClauseColumn of the ColumnName instance. 13 | // Clause 给 ColumnName 实例创建一个 ClauseColumn。 14 | func (columnName ColumnName[TYPE]) Clause() *ClauseColumn[TYPE] { 15 | return &ClauseColumn[TYPE]{ 16 | Table: "", 17 | Name: columnName.Name(), 18 | Alias: "", 19 | Raw: false, // 非 raw 的会被继续加工为合理的语句,比如增加表名,增加转义符号等,因此这里推荐使用 false(默认值) 20 | } 21 | } 22 | 23 | // ClauseWithTable creates a ClauseColumn with a specified table name. 24 | // ClauseWithTable 创建一个带有指定表名的 ClauseColumn。 25 | func (columnName ColumnName[TYPE]) ClauseWithTable(tableName string) *ClauseColumn[TYPE] { 26 | return &ClauseColumn[TYPE]{ 27 | Table: tableName, 28 | Name: columnName.Name(), 29 | Alias: "", 30 | Raw: false, // 非 raw 的会被继续加工为合理的语句,比如增加表名,增加转义符号等,因此这里推荐使用 false(默认值) 31 | } 32 | } 33 | 34 | // ClauseColumn represents a column with additional properties such as table, alias, and raw flag. 35 | // ClauseColumn 表示一个具有额外属性(如表名、别名和 raw 标志)的列。 36 | type ClauseColumn[TYPE any] clause.Column 37 | 38 | // WithTable sets the table name for the ClauseColumn and returns the updated ClauseColumn. 39 | // WithTable 为 ClauseColumn 设置表名并返回更新后的 ClauseColumn。 40 | func (clauseColumn *ClauseColumn[TYPE]) WithTable(tableName string) *ClauseColumn[TYPE] { 41 | clauseColumn.Table = tableName 42 | return clauseColumn 43 | } 44 | 45 | // WithAlias sets the alias for the ClauseColumn and returns the updated ClauseColumn. 46 | // WithAlias 为 ClauseColumn 设置别名并返回更新后的 ClauseColumn。 47 | func (clauseColumn *ClauseColumn[TYPE]) WithAlias(alias string) *ClauseColumn[TYPE] { 48 | clauseColumn.Alias = alias 49 | return clauseColumn 50 | } 51 | 52 | // WithRaw sets the raw flag for the ClauseColumn and returns the updated ClauseColumn. 53 | // WithRaw 为 ClauseColumn 设置 raw 标志并返回更新后的 ClauseColumn。 54 | func (clauseColumn *ClauseColumn[TYPE]) WithRaw(raw bool) *ClauseColumn[TYPE] { 55 | clauseColumn.Raw = raw 56 | return clauseColumn 57 | } 58 | 59 | // Column returns a GORM clause.Column, which represents a column in a SQL statement. 60 | // Column 返回一个 GORM 的 clause.Column,表示 SQL 语句中的一个列。 61 | func (clauseColumn *ClauseColumn[TYPE]) Column() clause.Column { 62 | return clause.Column(*clauseColumn) 63 | } 64 | 65 | // Assignment returns a GORM clause.Assignment, which is used to assign a value to a column. 66 | // Assignment 返回一个 GORM 的 clause.Assignment,用于将值赋给列。 67 | func (clauseColumn *ClauseColumn[TYPE]) Assignment(value TYPE) clause.Assignment { 68 | return clause.Assignment{Column: clauseColumn.Column(), Value: value} 69 | } 70 | -------------------------------------------------------------------------------- /internal/demos/demo1x/main.go: -------------------------------------------------------------------------------- 1 | // Package main demonstrates basic gormcnm usage with Account CRUD operations 2 | // Auto shows column definitions, WHERE queries, UPDATE operations, and expression updates 3 | // Runs with SQLite in-memory database to showcase type-safe column operations 4 | // 5 | // main 包演示 gormcnm 与 Account 增删改查操作的基本用法 6 | // 自动展示列定义、WHERE 查询、UPDATE 操作和表达式更新 7 | // 使用 SQLite 内存数据库运行以展示类型安全的列操作 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/google/uuid" 14 | "github.com/yyle88/done" 15 | "github.com/yyle88/gormcnm" 16 | "github.com/yyle88/neatjson/neatjsons" 17 | "github.com/yyle88/rese" 18 | "gorm.io/driver/sqlite" 19 | "gorm.io/gorm" 20 | "gorm.io/gorm/logger" 21 | ) 22 | 23 | type Account struct { 24 | Username string `gorm:"primary_key;type:varchar(100);"` 25 | Nickname string `gorm:"column:nickname;"` 26 | Age int `gorm:"column:age;"` 27 | } 28 | 29 | const ( 30 | columnUsername = gormcnm.ColumnName[string]("username") 31 | columnNickname = gormcnm.ColumnName[string]("nickname") 32 | columnAge = gormcnm.ColumnName[int]("age") 33 | ) 34 | 35 | func main() { 36 | dsn := fmt.Sprintf("file:db-%s?mode=memory&cache=shared", uuid.New().String()) 37 | db := rese.P1(gorm.Open(sqlite.Open(dsn), &gorm.Config{ 38 | Logger: logger.Default.LogMode(logger.Info), 39 | })) 40 | defer rese.F0(rese.P1(db.DB()).Close) 41 | 42 | //CREATE TABLE `accounts` (`username` varchar(100),`nickname` text,`age` integer,PRIMARY KEY (`username`)) 43 | done.Done(db.AutoMigrate(&Account{})) 44 | //INSERT INTO `accounts` (`username`,`nickname`,`age`) VALUES ("alice","Alice",17) 45 | done.Done(db.Create(&Account{Username: "alice", Nickname: "Alice", Age: 17}).Error) 46 | 47 | //SELECT * FROM `accounts` WHERE username="alice" ORDER BY `accounts`.`username` LIMIT 1 48 | var account Account 49 | done.Done(db.Where(columnUsername.Eq("alice")).First(&account).Error) 50 | fmt.Println(neatjsons.S(account)) 51 | 52 | //UPDATE `accounts` SET `nickname`="Alice-2" WHERE `username` = "alice" 53 | done.Done(db.Model(&account).Update(columnNickname.Kv("Alice-2")).Error) 54 | //SELECT * FROM `accounts` WHERE username="alice" ORDER BY `accounts`.`username` LIMIT 1 55 | done.Done(db.Where(columnUsername.Eq("alice")).First(&account).Error) 56 | fmt.Println(neatjsons.S(account)) 57 | 58 | //UPDATE `accounts` SET `age`=18,`nickname`="Alice-3" WHERE `username` = "alice" 59 | done.Done(db.Model(&account).Updates(columnNickname.Kw("Alice-3").Kw(columnAge.Kv(18)).AsMap()).Error) 60 | //SELECT * FROM `accounts` WHERE username="alice" ORDER BY `accounts`.`username` LIMIT 1 61 | done.Done(db.Where(columnUsername.Eq("alice")).First(&account).Error) 62 | fmt.Println(neatjsons.S(account)) 63 | 64 | //UPDATE `accounts` SET `age`=age + 1 WHERE `username` = "alice" 65 | done.Done(db.Model(&account).Update(columnAge.KeAdd(1)).Error) 66 | //SELECT * FROM `accounts` WHERE username="alice" ORDER BY `accounts`.`username` LIMIT 1 67 | done.Done(db.Where(columnUsername.Eq("alice")).First(&account).Error) 68 | fmt.Println(neatjsons.S(account)) 69 | } 70 | -------------------------------------------------------------------------------- /new_column_name.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides column name creation and decoration operations with flexible patterns 2 | // Auto creates ColumnName instances with type inference and customizable decoration strategies 3 | // Supports plain names, table-prefixed names, and custom transformation logic 4 | // 5 | // gormcnm 包提供列名创建和装饰操作,具有灵活的模式 6 | // 自动创建 ColumnName 实例,包含类型推断和可定制的装饰策略 7 | // 支持普通名称、表前缀名称和自定义转换逻辑 8 | package gormcnm 9 | 10 | import "github.com/yyle88/must" 11 | 12 | // New creates a new ColumnName with the specified type and name 13 | // New 创建一个带有指定类型和名称的新 ColumnName 14 | func New[T any](name string) ColumnName[T] { 15 | return ColumnName[T](name) 16 | } 17 | 18 | // Cnm creates a ColumnName using the type inferred from the value argument 19 | // Cnm 使用从值参数推断出的类型创建 ColumnName 20 | func Cnm[T any](v T, name string) ColumnName[T] { 21 | return ColumnName[T](name) 22 | } 23 | 24 | // Cmn creates a ColumnName with decoration applied to the name 25 | // Cmn 创建一个对名称应用装饰的 ColumnName 26 | func Cmn[T any](v T, name string, decoration ColumnNameDecoration) ColumnName[T] { 27 | return ColumnName[T](decoration.DecorateColumnName(name)) 28 | } 29 | 30 | // ColumnNameDecoration defines an interface to decorate column names 31 | // ColumnNameDecoration 定义装饰列名的接口 32 | type ColumnNameDecoration interface { 33 | DecorateColumnName(name string) string 34 | } 35 | 36 | // PlainDecoration provides simple decoration that returns the name unchanged 37 | // PlainDecoration 提供简单的装饰,返回不变的名称 38 | type PlainDecoration struct{} 39 | 40 | // NewPlainDecoration creates a new PlainDecoration instance 41 | // NewPlainDecoration 创建一个新的 PlainDecoration 实例 42 | func NewPlainDecoration() ColumnNameDecoration { 43 | return &PlainDecoration{} 44 | } 45 | 46 | // DecorateColumnName returns the column name without any modification 47 | // DecorateColumnName 返回未经任何修改的列名 48 | func (D *PlainDecoration) DecorateColumnName(name string) string { 49 | return name 50 | } 51 | 52 | // TableDecoration adds table prefix to column names 53 | // TableDecoration 为列名添加表前缀 54 | type TableDecoration struct { 55 | tableName string 56 | } 57 | 58 | // NewTableDecoration creates a new TableDecoration with the specified table name 59 | // NewTableDecoration 使用指定的表名创建一个新的 TableDecoration 60 | func NewTableDecoration(tableName string) ColumnNameDecoration { 61 | return &TableDecoration{tableName: tableName} 62 | } 63 | 64 | // DecorateColumnName adds table prefix to the column name when table name is not a blank string 65 | // DecorateColumnName 如果表名不为空,则为列名添加表前缀 66 | func (D *TableDecoration) DecorateColumnName(name string) string { 67 | if D.tableName != "" { 68 | return D.tableName + "." + name 69 | } 70 | return name 71 | } 72 | 73 | // CustomDecoration allows custom decoration logic via a function 74 | // CustomDecoration 允许通过函数实现自定义装饰逻辑 75 | type CustomDecoration struct { 76 | decorateFunc func(string) string 77 | } 78 | 79 | // NewCustomDecoration creates a new CustomDecoration with the provided function 80 | // NewCustomDecoration 使用提供的函数创建一个新的 CustomDecoration 81 | func NewCustomDecoration(decorateFunc func(name string) string) ColumnNameDecoration { 82 | return &CustomDecoration{decorateFunc: decorateFunc} 83 | } 84 | 85 | // DecorateColumnName applies the custom decoration function to the column name 86 | // DecorateColumnName 将自定义装饰函数应用到列名上 87 | func (D *CustomDecoration) DecorateColumnName(name string) string { 88 | return must.Nice(D.decorateFunc(name)) 89 | } 90 | -------------------------------------------------------------------------------- /select_statement.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides SELECT statement building operations for custom column selection 2 | // Auto constructs SELECT clauses with column combinations, aliases, and aggregate functions 3 | // Supports building complex SELECT queries with type-safe column management and GORM integration 4 | // 5 | // gormcnm 提供 SELECT 语句构建操作,用于自定义列选择 6 | // 自动构建 SELECT 子句,包含列组合、别名和聚合函数 7 | // 支持构建复杂的 SELECT 操作,具备类型安全的列管理和 GORM 集成 8 | package gormcnm 9 | 10 | import ( 11 | "strings" 12 | 13 | "gorm.io/gorm" 14 | ) 15 | 16 | // SxType is an alias when using SelectStatement as a short type name 17 | // SxType 是 SelectStatement 的别名,用作简短的类型名称 18 | type SxType = SelectStatement 19 | 20 | // NewSx creates a new SelectStatement instance with the provided statement and arguments 21 | // NewSx 使用提供的语句和参数创建一个新的 SelectStatement 实例 22 | func NewSx(stmt string, args ...interface{}) *SxType { 23 | return &SxType{ 24 | statementArgumentsTuple: newStatementArgumentsTuple(stmt, args), 25 | } 26 | } 27 | 28 | // SelectStatement represents a SELECT statement with arguments used in GORM db.Select operations 29 | // Handles complex SELECT scenarios where conditions and arguments are needed 30 | // Auto combines multiple select statements with comma separation to form multi-column queries 31 | // 32 | // Usage scenarios: 33 | // - 99% of cases db.Select requires no parameters 34 | // - However, with complex queries such as: SELECT COUNT(CASE WHEN condition THEN 1 END) as cnt 35 | // - Need to merge multiple column select statements and corresponding parameters 36 | // - Columns are separated using commas 37 | // 38 | // SelectStatement 表示用于 GORM db.Select 操作的 SELECT 语句及参数 39 | // 处理需要条件和参数的复杂 SELECT 场景 40 | // 自动使用逗号分隔合并多个 SELECT 语句进行多列查询 41 | // 42 | // 使用场景说明: 43 | // - 99%的情况下 db.Select 不需要参数 44 | // - 但对于复杂查询如:SELECT COUNT(CASE WHEN condition THEN 1 END) as cnt 45 | // - 需要合并多个列的 SELECT 语句和对应参数 46 | // - 各列之间使用逗号分隔 47 | type SelectStatement struct { 48 | *statementArgumentsTuple // Embedded statement-arguments tuple // 嵌入的语句-参数元组 49 | } 50 | 51 | // NewSelectStatement creates a new SelectStatement with the provided query string and arguments. 52 | // NewSelectStatement 使用提供的查询字符串和参数创建一个新的 SelectStatement 实例。 53 | func NewSelectStatement(stmt string, args ...interface{}) *SelectStatement { 54 | return &SelectStatement{ 55 | statementArgumentsTuple: newStatementArgumentsTuple(stmt, args), 56 | } 57 | } 58 | 59 | // Combine combines the current SelectStatement with other SelectStatements by merging their query strings and arguments. 60 | // Combine 将当前的 SelectStatement 与其他 SelectStatement 合并,通过合并它们的查询字符串和参数。 61 | func (sx *SelectStatement) Combine(cs ...*SelectStatement) *SelectStatement { 62 | var qsVs []string 63 | qsVs = append(qsVs, sx.Qs()) 64 | var args []any 65 | args = append(args, sx.Args()...) 66 | for _, c := range cs { 67 | qsVs = append(qsVs, c.Qs()) 68 | args = append(args, c.Args()...) 69 | } 70 | var stmt = strings.Join(qsVs, ", ") //得到的就是gorm db.Select() 的要选中的列信息,因此使用逗号分隔 71 | return NewSelectStatement(stmt, args...) //得到的就是 gorm db.Select() 的选中信息和附带的参数信息,比如 COUNT(CASE WHEN condition THEN 1 END) 里 condition 的参数信息 72 | } 73 | 74 | // Scope converts the SelectStatement to a GORM ScopeFunction used with db.Scopes(). 75 | // It applies the SELECT operation defined by SelectStatement to the GORM select. 76 | // Scope 将 SelectStatement 转换为 GORM 的 ScopeFunction,以便于被 db.Scopes() 调用。 77 | // 它将 SelectStatement 定义的查询语句应用于 GORM 查询。 78 | func (sx *SelectStatement) Scope() ScopeFunction { 79 | return func(db *gorm.DB) *gorm.DB { 80 | return db.Select(sx.Qs(), sx.args...) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: create-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 监听 main 分支的 push 操作(编译和测试/代码检查) 7 | tags: 8 | - 'v*' # 监听以 'v' 开头的标签的 push 操作(发布 Release) 9 | 10 | jobs: 11 | lint: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: actions/setup-go@v6 17 | with: 18 | go-version: stable 19 | cache: true 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v9 22 | with: 23 | version: latest 24 | args: --timeout=5m 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | go: [ "1.22.x", "1.23.x", "1.24.x", "1.25.x", "stable" ] 31 | steps: 32 | - uses: actions/checkout@v6 33 | 34 | - uses: actions/setup-go@v6 35 | with: 36 | go-version: ${{ matrix.go }} 37 | cache: true 38 | 39 | - name: Run govulncheck 40 | uses: golang/govulncheck-action@v1 41 | with: 42 | go-version-input: ${{ matrix.go }} 43 | go-package: ./... 44 | continue-on-error: true # 报错时允许工作流继续执行,因为项目依赖的底层包也会有错,很难做到百分百没问题,只打印检测结果就行 45 | 46 | - name: Run test 47 | run: make test COVERAGE_DIR=/tmp/coverage 48 | 49 | - name: Upload test results 50 | uses: actions/upload-artifact@v4 51 | if: always() 52 | with: 53 | name: test-results-${{ matrix.go }} 54 | path: /tmp/coverage/ 55 | retention-days: 30 56 | 57 | - name: Send goveralls coverage 58 | uses: shogo82148/actions-goveralls@v1 59 | with: 60 | path-to-profile: /tmp/coverage/combined.txt 61 | flag-name: Go-${{ matrix.go }} 62 | parallel: true 63 | if: ${{ github.event.repository.fork == false }} # 仅在非 fork 时上传覆盖率 64 | 65 | check-coverage: 66 | name: Check coverage 67 | needs: [ test ] 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: shogo82148/actions-goveralls@v1 71 | with: 72 | parallel-finished: true 73 | if: ${{ github.event.repository.fork == false }} # 仅在非 fork 时检查覆盖率 74 | 75 | # 代码质量分析 76 | code-analysis: 77 | name: CodeQL Analysis 78 | runs-on: ubuntu-latest 79 | permissions: 80 | actions: read 81 | contents: read 82 | security-events: write 83 | steps: 84 | - name: Checkout repository 85 | uses: actions/checkout@v6 86 | 87 | - name: Initialize CodeQL 88 | uses: github/codeql-action/init@v4 89 | with: 90 | languages: go 91 | 92 | - name: Auto Build 93 | uses: github/codeql-action/autobuild@v4 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v4 97 | 98 | # 发布 Release 99 | release: 100 | name: Release a new version 101 | needs: [ lint, test, check-coverage, code-analysis ] 102 | runs-on: ubuntu-latest 103 | # 仅在推送标签时执行 - && - 仅在非 fork 时执行发布 104 | if: ${{ github.event.repository.fork == false && success() && startsWith(github.ref, 'refs/tags/v') }} 105 | steps: 106 | # 1. 检出代码 107 | - name: Checkout code 108 | uses: actions/checkout@v6 109 | with: 110 | fetch-depth: 0 # 获取完整历史用于生成更好的 release notes 111 | 112 | # 2. 创建 Release 和上传源码包 113 | - name: Create Release 114 | uses: softprops/action-gh-release@v2 115 | with: 116 | generate_release_notes: true 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /qs_coalesce.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides COALESCE and IFNULL operations for NULL-safe SQL queries 2 | // Auto handles NULL values in aggregate functions using COALESCE (standard) or IFNULL (MySQL) 3 | // Supports SUM, COUNT, AVG, MAX, MIN with automatic NULL value protection 4 | // 5 | // gormcnm 提供 COALESCE 和 IFNULL 操作,实现 NULL 安全的 SQL 查询 6 | // 自动使用 COALESCE(标准)或 IFNULL(MySQL)处理聚合函数中的 NULL 值 7 | // 支持 SUM、COUNT、AVG、MAX、MIN,具备自动 NULL 值保护 8 | package gormcnm 9 | 10 | import ( 11 | "github.com/yyle88/gormcnm/internal/utils" 12 | "github.com/yyle88/tern/zerotern" 13 | ) 14 | 15 | // COALESCE creates a COALESCE function wrapper for handling NULL values in SQL queries 16 | // Auto uses SQL standard COALESCE function, supported by most database systems 17 | // COALESCE 为处理 SQL 查询中的 NULL 值创建 COALESCE 函数包装器 18 | // 自动使用 SQL 标准的 COALESCE 函数,被大多数数据库系统支持 19 | func (columnName ColumnName[TYPE]) COALESCE() *CoalesceNonNullGuardian { 20 | return NewCoalesceNonNullGuardian("COALESCE", string(columnName)) 21 | } 22 | 23 | // IFNULLFN creates an IFNULL function wrapper for MySQL-specific NULL handling 24 | // MySQL-specific function, may not be supported in other database systems 25 | // IFNULLFN 为 MySQL 特定的 NULL 处理创建 IFNULL 函数包装器 26 | // MySQL 特定函数,在其他数据库系统中可能不受支持 27 | func (columnName ColumnName[TYPE]) IFNULLFN() *CoalesceNonNullGuardian { 28 | return NewCoalesceNonNullGuardian("IFNULL", string(columnName)) 29 | } 30 | 31 | // CoalesceNonNullGuardian provides SQL aggregate functions with NULL value protection 32 | // Auto handles NULL values using COALESCE or IFNULL functions to provide default values 33 | // CoalesceNonNullGuardian 提供带有 NULL 值保护的 SQL 聚合函数 34 | // 自动使用 COALESCE 或 IFNULL 函数处理 NULL 值以提供默认值 35 | type CoalesceNonNullGuardian struct { 36 | method string // SQL function name (COALESCE or IFNULL) // SQL 函数名(COALESCE 或 IFNULL) 37 | column string // Column name to apply the function to // 要应用函数的列名 38 | } 39 | 40 | // NewCoalesceNonNullGuardian creates a new CoalesceNonNullGuardian with specified method and column 41 | // NewCoalesceNonNullGuardian 使用指定的方法和列名创建新的 CoalesceNonNullGuardian 42 | func NewCoalesceNonNullGuardian(methodName string, columnName string) *CoalesceNonNullGuardian { 43 | return &CoalesceNonNullGuardian{ 44 | method: methodName, 45 | column: columnName, 46 | } 47 | } 48 | 49 | // Stmt generates an SQL statement for the COALESCE or IFNULL function with the given function and default value. 50 | // Stmt 生成一个 SQL 语句,包含 COALESCE 或 IFNULL 函数,并指定默认值。 51 | func (qs *CoalesceNonNullGuardian) Stmt(sfn string, dfv string, alias string) string { 52 | return utils.ApplyAliasToColumn(qs.method+"("+sfn+"("+string(qs.column)+"), "+zerotern.VV(dfv, "0")+")", alias) 53 | } 54 | 55 | // SumStmt generates an SQL statement to calculate the sum of the column, using 0 as the default value. 56 | // SumStmt 生成一个 SQL 语句,计算列的总和,默认值为 0。 57 | func (qs *CoalesceNonNullGuardian) SumStmt(alias string) string { 58 | return qs.Stmt("SUM", "0", alias) 59 | } 60 | 61 | // MaxStmt generates an SQL statement to retrieve the maximum value of the column, using 0 as the default value. 62 | // MaxStmt 生成一个 SQL 语句,检索列的最大值,默认值为 0。 63 | func (qs *CoalesceNonNullGuardian) MaxStmt(alias string) string { 64 | return qs.Stmt("MAX", "0", alias) 65 | } 66 | 67 | // MinStmt generates an SQL statement to retrieve the minimum value of the column, using 0 as the default value. 68 | // MinStmt 生成一个 SQL 语句,检索列的最小值,默认值为 0。 69 | func (qs *CoalesceNonNullGuardian) MinStmt(alias string) string { 70 | return qs.Stmt("MIN", "0", alias) 71 | } 72 | 73 | // AvgStmt generates an SQL statement to calculate the average value of the column, using 0 as the default value. 74 | // AvgStmt 生成一个 SQL 语句,计算列的平均值,默认值为 0。 75 | func (qs *CoalesceNonNullGuardian) AvgStmt(alias string) string { 76 | return qs.Stmt("AVG", "0", alias) 77 | } 78 | -------------------------------------------------------------------------------- /gormcnmstub/stub.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated using sure/cls_stub_gen. DO NOT EDIT. 2 | // This file was auto generated via github.com/yyle88/sure 3 | // Generated from: stub.gen_test.go:35 -> gormcnmstub.TestGen 4 | // ========== SURE:DO-NOT-EDIT-SECTION:END ========== 5 | 6 | package gormcnmstub 7 | 8 | import ( 9 | "github.com/yyle88/gormcnm" 10 | "github.com/yyle88/gormcnm/internal/utils" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | func OK() bool { 15 | return stub.OK() 16 | } 17 | func CreateCondition(stmt string, args ...interface{}) *gormcnm.QxConjunction { 18 | return stub.CreateCondition(stmt, args...) 19 | } 20 | func NewQx(stmt string, args ...interface{}) *gormcnm.QxConjunction { 21 | return stub.NewQx(stmt, args...) 22 | } 23 | func Qx(stmt string, args ...interface{}) *gormcnm.QxConjunction { 24 | return stub.Qx(stmt, args...) 25 | } 26 | func CreateSelect(stmt string, args ...interface{}) *gormcnm.SelectStatement { 27 | return stub.CreateSelect(stmt, args...) 28 | } 29 | func NewSx(stmt string, args ...interface{}) *gormcnm.SelectStatement { 30 | return stub.NewSx(stmt, args...) 31 | } 32 | func Sx(stmt string, args ...interface{}) *gormcnm.SelectStatement { 33 | return stub.Sx(stmt, args...) 34 | } 35 | func NewColumnValueMap() gormcnm.ColumnValueMap { 36 | return stub.NewColumnValueMap() 37 | } 38 | func NewKw() gormcnm.ColumnValueMap { 39 | return stub.NewKw() 40 | } 41 | func CreateColumnValueMap(columnName string, value interface{}) gormcnm.ColumnValueMap { 42 | return stub.CreateColumnValueMap(columnName, value) 43 | } 44 | func Kw(columnName string, value interface{}) gormcnm.ColumnValueMap { 45 | return stub.Kw(columnName, value) 46 | } 47 | func Where(db *gorm.DB, qxs ...*gormcnm.QxConjunction) *gorm.DB { 48 | return stub.Where(db, qxs...) 49 | } 50 | func OrderByColumns(db *gorm.DB, obs ...gormcnm.OrderByBottle) *gorm.DB { 51 | return stub.OrderByColumns(db, obs...) 52 | } 53 | func UpdateColumns(db *gorm.DB, kws ...gormcnm.ColumnValueMap) *gorm.DB { 54 | return stub.UpdateColumns(db, kws...) 55 | } 56 | func CombineColumnNames(a ...utils.ColumnNameInterface) string { 57 | return stub.CombineColumnNames(a...) 58 | } 59 | func MergeNames(a ...utils.ColumnNameInterface) string { 60 | return stub.MergeNames(a...) 61 | } 62 | func CombineNamesSlices(a ...[]string) string { 63 | return stub.CombineNamesSlices(a...) 64 | } 65 | func MergeSlices(a ...[]string) string { 66 | return stub.MergeSlices(a...) 67 | } 68 | func CombineStatements(a ...string) string { 69 | return stub.CombineStatements(a...) 70 | } 71 | func MergeStmts(a ...string) string { 72 | return stub.MergeStmts(a...) 73 | } 74 | func CountStmt(alias string) string { 75 | return stub.CountStmt(alias) 76 | } 77 | func CountCaseWhenStmt(condition string, alias string) string { 78 | return stub.CountCaseWhenStmt(condition, alias) 79 | } 80 | func CountCaseWhenQxSx(qx *gormcnm.QxConjunction, alias string) *gormcnm.SelectStatement { 81 | return stub.CountCaseWhenQxSx(qx, alias) 82 | } 83 | func CombineSelectStatements(cs ...gormcnm.SelectStatement) *gormcnm.SelectStatement { 84 | return stub.CombineSelectStatements(cs...) 85 | } 86 | func CombineSxs(cs ...gormcnm.SelectStatement) *gormcnm.SelectStatement { 87 | return stub.CombineSxs(cs...) 88 | } 89 | func Select(db *gorm.DB, qxs ...*gormcnm.SelectStatement) *gorm.DB { 90 | return stub.Select(db, qxs...) 91 | } 92 | func LEFTJOIN(tableName string) *gormcnm.TableJoin { 93 | return stub.LEFTJOIN(tableName) 94 | } 95 | func RIGHTJOIN(tableName string) *gormcnm.TableJoin { 96 | return stub.RIGHTJOIN(tableName) 97 | } 98 | func INNERJOIN(tableName string) *gormcnm.TableJoin { 99 | return stub.INNERJOIN(tableName) 100 | } 101 | func CROSSJOIN(tableName string) *gormcnm.TableJoin { 102 | return stub.CROSSJOIN(tableName) 103 | } 104 | -------------------------------------------------------------------------------- /qs_conjunction.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides conjunction operations to build complex WHERE clauses 2 | // Auto combines multiple conditions using AND, OR, NOT boolean operators 3 | // Supports nested conditions and flexible queries composition with type-safe SQL generation 4 | // 5 | // gormcnm 提供查询语句连接词操作,用于构建复杂的 WHERE 子句 6 | // 自动使用 AND、OR、NOT 逻辑运算符组合多个条件 7 | // 支持嵌套条件和灵活的查询组合,具备类型安全的 SQL 生成 8 | package gormcnm 9 | 10 | import ( 11 | "database/sql/driver" 12 | "fmt" 13 | "strings" 14 | ) 15 | 16 | // QsType is an alias when using QsConjunction as a short type name 17 | // QsType 是 QsConjunction 的别名,用作简短的类型名称 18 | type QsType = QsConjunction 19 | 20 | // NewQs creates a new QsConjunction instance from the given SQL statement string 21 | // NewQs 从给定的 SQL 语句字符串创建一个新的 QsConjunction 实例 22 | func NewQs(stmt string) QsType { 23 | return QsType(stmt) 24 | } 25 | 26 | // QsConjunction means gorm queries conjunction. example: OR AND NOT 27 | // QsConjunction 表示 GORM 查询语句中的连接词,例如 OR、AND、NOT 28 | // 就是表示 "或"、"且"、"非" 的连接词 29 | // 在语法学中,conjunction(连词)是一种词类,用来连接词、短语、语句 30 | type QsConjunction string 31 | 32 | // NewQsConjunction creates a new QsConjunction instance from the given SQL statement string 33 | // NewQsConjunction 从给定的 SQL 语句字符串创建一个新的 QsConjunction 实例 34 | func NewQsConjunction(stmt string) QsConjunction { 35 | return QsConjunction(stmt) 36 | } 37 | 38 | // AND constructs a conjunction using "AND" between QsConjunction instances. 39 | // AND 使用 "AND" 在多个 QsConjunction 实例之间构造连接语句。 40 | func (qsConjunction QsConjunction) AND(qcs ...QsConjunction) QsConjunction { 41 | var qss = make([]string, 0, 1+len(qcs)) // New slice to ensure thread-safe operation // 新建一个切片以确保线程安全 42 | qss = append(qss, "("+qsConjunction.Qs()+")") 43 | for _, c := range qcs { 44 | qss = append(qss, "("+c.Qs()+")") // Add parentheses around each component to avoid logic issues // 在每个组件周围加括号以避免逻辑问题 45 | } 46 | return "(" + QsConjunction(strings.Join(qss, " AND ")) + ")" 47 | } 48 | 49 | // OR constructs a conjunction using "OR" between QsConjunction instances. 50 | // OR 使用 "OR" 在多个 QsConjunction 实例之间构造连接语句。 51 | func (qsConjunction QsConjunction) OR(qcs ...QsConjunction) QsConjunction { 52 | var qss = make([]string, 0, 1+len(qcs)) // New slice to ensure thread-safe operation // 新建一个切片以确保线程安全 53 | qss = append(qss, "("+qsConjunction.Qs()+")") 54 | for _, c := range qcs { 55 | qss = append(qss, "("+c.Qs()+")") // Add parentheses around each component to avoid logic issues // 在每个组件周围加括号以避免逻辑问题 56 | } 57 | return "(" + QsConjunction(strings.Join(qss, " OR ")) + ")" 58 | } 59 | 60 | // NOT negates the QsConjunction instance by wrapping it with "NOT". 61 | // NOT 通过添加 "NOT" 来对 QsConjunction 实例进行逻辑取反。 62 | func (qsConjunction QsConjunction) NOT() QsConjunction { 63 | return QsConjunction(fmt.Sprintf("NOT(%s)", qsConjunction)) 64 | } 65 | 66 | // Value prevents GORM from directly using QsConjunction by causing a panic. 67 | // Value 阻止 GORM 直接使用 QsConjunction,通过触发 panic 实现。 68 | // 当你定义了一个类型为 type xxx string 的自定义类型,并尝试将其用作查询条件时,GORM 会将整个自定义类型作为一个值来处理,而不是将其展开为 SQL 语句中的占位符。 69 | // 因此也就是说 db.Where(xxx("name = ? AND type = ?")) 这个是不行的 70 | // 基本的结论是: 71 | // 当你执行 db.Where(xxx("name = ? AND type = ?")) 时,GORM 会将 xxx("name = ? AND type = ?") 视为一个单独的值,再将其传递给查询条件,而不会将其展开为具体的 SQL 语句。 72 | // 这时,给 type xxx string 增加 Value 函数,而且里面报 panic,就能避免用错 73 | // 这就是这里给出 Value { panic } 的原因 74 | // 这样,假如你不把结果转换为 string,而是直接往 where 条件里传递,就会在 where 条件中触发 value 异常 75 | func (qsConjunction QsConjunction) Value() (driver.Value, error) { 76 | panic(valueIsNotCallable) // If this error occurs, the caller must adjust their code. See error code comments. 77 | // 如果报这个错误,调用侧需要修改代码。具体可参考错误码的注释。 78 | } 79 | 80 | // Qs convert the QsConjunction instance into a string representation. 81 | // Qs 将 QsConjunction 实例转换为字符串表示。 82 | func (qsConjunction QsConjunction) Qs() string { 83 | return string(qsConjunction) 84 | } 85 | 86 | // Qx converts the QsConjunction instance into a QxConjunction object with arguments. 87 | // Qx 将 QsConjunction 实例转换为带参数的 QxConjunction 对象。 88 | func (qsConjunction QsConjunction) Qx() *QxConjunction { 89 | var args = make([]interface{}, 0) // Means no args // 意味着没有参数 90 | return NewQxConjunction(string(qsConjunction), args...) 91 | } 92 | -------------------------------------------------------------------------------- /value_map.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides column-value map operations for GORM update and batch operations 2 | // Auto manages column-value mappings for Updates and UpdateColumns operations 3 | // Supports building dynamic update maps with type-safe column assignments 4 | // 5 | // gormcnm 提供列值映射操作,用于 GORM 更新和批量操作 6 | // 自动管理 Updates 和 UpdateColumns 操作的列值映射 7 | // 支持构建动态更新映射,具备类型安全的列赋值 8 | package gormcnm 9 | 10 | // ColumnValueMap is a map type used for GORM update logic to represent column-value mappings. 11 | // This type is used when calling gormrepo.Updates, gormrepo.UpdatesM, or GORM native db.Updates. 12 | // It provides type-safe column-value assignments with chainable Kw() method. 13 | // 14 | // ColumnValueMap 是一个用于 GORM 更新逻辑的映射类型,用于表示列与值的对应关系表。 15 | // 该类型在调用 gormrepo.Updates、gormrepo.UpdatesM 或 GORM 原生 db.Updates 时使用。 16 | // 提供类型安全的列值赋值,支持链式调用 Kw() 方法。 17 | // 18 | // Usage with gormrepo.Updates (requires AsMap() conversion): 19 | // 使用 gormrepo.Updates 时(需要 AsMap() 转换): 20 | // 21 | // repo.Updates(where, func(cls *AccountColumns) map[string]interface{} { 22 | // return cls. 23 | // Kw(cls.Nickname.Kv(newNickname)). 24 | // Kw(cls.Password.Kv(newPassword)). 25 | // AsMap() // Convert to map[string]interface{} // 转换为 map[string]interface{} 类型 26 | // }) 27 | // 28 | // Usage with gormrepo.UpdatesM (no AsMap() needed): 29 | // 使用 gormrepo.UpdatesM 时(无需 AsMap()): 30 | // 31 | // repo.UpdatesM(where, func(cls *AccountColumns) gormcnm.ColumnValueMap { 32 | // return cls. 33 | // Kw(cls.Nickname.Kv(newNickname)). 34 | // Kw(cls.Password.Kv(newPassword)) 35 | // // No AsMap() needed! // 不需要 AsMap()! 36 | // }) 37 | type ColumnValueMap map[string]interface{} 38 | 39 | // NewKw initializes a new ColumnValueMap. 40 | // NewKw 初始化一个新的 ColumnValueMap。 41 | func NewKw() ColumnValueMap { 42 | return make(ColumnValueMap) 43 | } 44 | 45 | // Kw creates a new ColumnValueMap and adds a key-value. 46 | // Kw 创建一个新的 ColumnValueMap 并添加一个键值对。 47 | func Kw(columnName string, value interface{}) ColumnValueMap { 48 | return NewKw().Kw(columnName, value) 49 | } 50 | 51 | // Kw adds a key-value pair to ColumnValueMap and supports chaining. 52 | // Kw 向 ColumnValueMap 添加键值对并支持链式调用。 53 | func (mp ColumnValueMap) Kw(columnName string, value interface{}) ColumnValueMap { 54 | mp[columnName] = value 55 | return mp 56 | } 57 | 58 | // Kws converts ColumnValueMap to map[string]interface{} for GORM. 59 | // Required when using gormrepo.Updates, not needed when using gormrepo.UpdatesM. 60 | // Recommend using AsMap() instead, as it has cleaner semantics and avoids naming conflicts. 61 | // 62 | // Kws 将 ColumnValueMap 转换为 GORM 的 map[string]interface{}。 63 | // 使用 gormrepo.Updates 时需要调用,使用 gormrepo.UpdatesM 时不需要。 64 | // 推荐使用 AsMap(),语义更明确且不易与其他名称冲突。 65 | func (mp ColumnValueMap) Kws() map[string]interface{} { 66 | return mp 67 | } 68 | 69 | // Map converts ColumnValueMap to map[string]interface{} for GORM. 70 | // Required when using gormrepo.Updates, not needed when using gormrepo.UpdatesM. 71 | // Recommend using AsMap() instead, as it has cleaner semantics and avoids naming conflicts. 72 | // 73 | // Map 将 ColumnValueMap 转换为 map[string]interface{}。 74 | // 使用 gormrepo.Updates 时需要调用,使用 gormrepo.UpdatesM 时不需要。 75 | // 推荐使用 AsMap(),语义更明确且不易与其他名称冲突。 76 | func (mp ColumnValueMap) Map() map[string]interface{} { 77 | return mp 78 | } 79 | 80 | // AsMap converts ColumnValueMap to map[string]interface{} for GORM. 81 | // Required when using gormrepo.Updates, not needed when using gormrepo.UpdatesM. 82 | // This is the recommended conversion method with clear semantics. 83 | // 84 | // AsMap 将 ColumnValueMap 转换为 map[string]interface{}。 85 | // 使用 gormrepo.Updates 时需要调用,使用 gormrepo.UpdatesM 时不需要。 86 | // 这是推荐的转换方法,语义清晰明确。 87 | func (mp ColumnValueMap) AsMap() map[string]interface{} { 88 | return mp 89 | } 90 | 91 | // ToMap converts ColumnValueMap to map[string]interface{} for GORM. 92 | // Required when using gormrepo.Updates, not needed when using gormrepo.UpdatesM. 93 | // Recommend using AsMap() instead, as it has cleaner semantics and avoids naming conflicts. 94 | // 95 | // ToMap 将 ColumnValueMap 转换为 map[string]interface{}。 96 | // 使用 gormrepo.Updates 时需要调用,使用 gormrepo.UpdatesM 时不需要。 97 | // 推荐使用 AsMap(),语义更明确且不易与其他名称冲突。 98 | func (mp ColumnValueMap) ToMap() map[string]interface{} { 99 | return mp 100 | } 101 | -------------------------------------------------------------------------------- /internal/docs/README_OLD_DOC.zh.md: -------------------------------------------------------------------------------- 1 | # gormcnm 根据 golang 定义的 models struct 字段调用 gorm 的增删改查 2 | 3 | ## 使用 4 | 假设你的 model 定义是这样的: 5 | ``` 6 | type Example struct { 7 | Name string `gorm:"primary_key;type:varchar(100);"` 8 | Type string `gorm:"column:type;"` 9 | Rank int `gorm:"column:rank;"` 10 | } 11 | ``` 12 | 很明显的你会得到它们的列名是 `name` `type` `rank`,在使用gorm时,通常是这样查询的 13 | ``` 14 | err := db.Where("name=?", "abc").First(&res).Error 15 | ``` 16 | 由于 `name` 是个硬编码的字段,因此我们期望的时在 `Example` 里这个字段是稳定的,随着业务变化也不会被修改的(比如删除或者换类型) 17 | 由此就会让我们形成开发习惯,即定义models的时候需要慎重,因为它是一切逻辑的基石。 18 | 19 | 但是“确保models不常修改”,这其实是一种奢望。[想要解决这种奢望的创作背景](./CREATION_IDEAS.zh.md) 这个文档只是创作背景和意图,不看也罢,接下来说明如何使用: 20 | 21 | 简单demo: 22 | 23 | [简单demo](../demos/demo1x/main.go) | [简单demo](../demos/demo2x/main.go) 24 | 25 | 这些都是最简单的,但实际上也只是demo级别的 26 | 27 | 推荐你看这个项目里的demo: [自动生成 gormcnm 字段定义的工具 gormcngen](https://github.com/yyle88/gormcngen) 请在这个项目的README中找到demo代码,将会教会你如何更好的使用。 28 | 这个才是核心的,确实非常好用的。 29 | 30 | 首先假设你的模型是 31 | ``` 32 | type Example struct { 33 | Name string `gorm:"primary_key;type:varchar(100);"` 34 | Type string `gorm:"column:type;"` 35 | Rank int `gorm:"column:rank;"` 36 | } 37 | ``` 38 | 这个模型的各个列字段定义就是这样的 39 | ``` 40 | const ( 41 | columnName = gormcnm.ColumnName[string]("name") 42 | columnType = gormcnm.ColumnName[string]("type") 43 | columnRank = gormcnm.ColumnName[int]("rank") 44 | ) 45 | ``` 46 | 这时候使用 gorm 查询就是这样的: 47 | ``` 48 | //SELECT * FROM `examples` WHERE name="abc" AND type="xyz" AND rank>100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 49 | var res Example 50 | if err := db.Where(columnName.Eq("abc")). 51 | Where(columnType.Eq("xyz")). 52 | Where(columnRank.Gt(100)). 53 | Where(columnRank.Lt(200)). 54 | First(&res).Error; err != nil { 55 | panic(errors.WithMessage(err, "wrong")) 56 | } 57 | fmt.Println(res) 58 | ``` 59 | 这样查询就能避免代码中出现大量的列名字符串,确保在重构代码的时候不会漏改,或者字段类型设置错误。 60 | 61 | 比如 name 是 string 而 rank 是 int 的,这时候使用 62 | ``` 63 | db.Where("name=?", 1).First(&res).Error //就会报错,这是因为 name 是 string 的,查询结果肯定是不正确的 64 | ``` 65 | 通常的假如使用 66 | ``` 67 | db.Where("name=?","abc).UpdateColumns(map[string]any{"rank":"xyz"}) //也会报错,因为 rank 是 int 而非 string 类型 68 | ``` 69 | 由于查询时 db.Where 需要的是 `interface{}` 类型参数,而更新的时候传的是 `map[string]any` 类型参数 70 | 就使得在静态检查/编译阶段,都不能即时发现错误,只在业务运行时报错,这就不利于快速重构和发现问题。 71 | 72 | 而使用我的工具就能避免这个问题。 73 | 74 | 假如不想手写各个类名的常数列表,还可以使用配套的自动化生成工具,自动化生成自定义的所有 models 的列名。 75 | 76 | 调用工具将生成常量配置的代码。这是工具项目 [自动生成 gormcnm 字段定义的工具包 gormcngen](https://github.com/yyle88/gormcngen) 只看README就行。 77 | 78 | 自动化生成的列名定义是这样的(这里为了防止各个模型都有相同的字段,比如 id created_at updated_at deleted_at,就使用了个自定义的类将它们装起来): 79 | ``` 80 | type ExampleColumns struct { 81 | Name gormcnm.ColumnName[string] 82 | Type gormcnm.ColumnName[string] 83 | Rank gormcnm.ColumnName[int] 84 | } 85 | 86 | func (*Example) Columns() *ExampleColumns { 87 | return &ExampleColumns{ 88 | Name: "name", 89 | Type: "type", 90 | Rank: "rank", 91 | } 92 | } 93 | ``` 94 | 接下来使用 95 | ``` 96 | // SELECT * FROM `examples` WHERE name="abc" AND type="xyz" AND rank>100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 97 | var res models.Example 98 | var cls = res.Columns() 99 | if err := db.Where(cls.Name.Eq("abc")). 100 | Where(cls.Type.Eq("xyz")). 101 | Where(cls.Rank.Gt(100)). 102 | Where(cls.Rank.Lt(200)). 103 | First(&res).Error; err != nil { 104 | panic(errors.WithMessage(err, "wrong")) //只是demo这样写写 105 | } 106 | fmt.Println(res) 107 | ``` 108 | 其它查询逻辑都有封装,具体在开发中可慢慢体会如何使用。 109 | 110 | ## 安装 111 | 使用本项目 112 | ``` 113 | go get github.com/yyle88/gormcnm 114 | ``` 115 | 116 | ``` 117 | import "github.com/yyle88/gormcnm" 118 | ``` 119 | 120 | ## 优势 121 | 有诸多优势,有的写在了开发思路文档里 [开发思路](./CREATION_IDEAS.zh.md) 122 | 这里补充些显而易见的优势 123 | 1. 能够让你的代码重构变得安全,特别是重命名字段/修改字段类型/删除字段时,它能让你能在静态检查阶段发现问题。 124 | 2. 能够让你的重构变得便捷,比如当想修改 `Name` -> `Username` 的时候,就直接通过IDE的重命名(比如使用GOLAND的shift+F6快捷键),把模型里的 `Name` 改为 `Username` 再把 `ExampleColumns . Name` 重命名,把 `Name: "name"` 修改为 `Username: "username"` 就行,这就能保证你不会改到别的表,非常便捷。 125 | 3. 能够提高你的编码速度,就比如 `db.Where(cls.Name.Eq("abc"))` 就会比 `db.Where("name=?", "abc")` 的编码效率高,而当字段名比较长的时候这种优势会很明显(毕竟有IDE代码自动提示,只要输`cls.`就能自动提示后面的)。 126 | 4. 能够让你的代码搜索更方便,比如想搜索db中 `name` 改变的原因,通过搜索模型中 `Name` 的引用,就能在 `db.Save` 或 `db.Create` 里面找到赋值的代码位置,再搜索 `cls.Name` 的引用,就能找到`db.Update` `db.UpdateColumn` `db.Updates` 里跟设置 `Name` 相关的代码位置。 127 | 128 | Give me stars. Thank you!!! 129 | -------------------------------------------------------------------------------- /table_column.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides table-qualified column operations to support multi-table data retrieval 2 | // Auto creates columns with table prefixes to facilitate JOIN operations and complex data retrieval 3 | // Supports building complete column names with table association 4 | // 5 | // gormcnm 提供表限定列操作,用于多表查询 6 | // 自动创建带表前缀的列,用于 JOIN 操作和复杂查询 7 | // 支持构建完全限定的列名,具备表关联功能 8 | package gormcnm 9 | 10 | import "github.com/yyle88/gormcnm/internal/utils" 11 | 12 | // TB creates a TableColumn through associating this column with a table interface 13 | // TB 通过将此列与表接口关联创建 TableColumn 14 | func (columnName ColumnName[TYPE]) TB(tab utils.GormTableNameFace) *TableColumn[TYPE] { 15 | return columnName.WithTable(tab) 16 | } 17 | 18 | // TC creates a TableColumn through associating this column with a table interface (alias of TB) 19 | // TC 通过将此列与表接口关联创建 TableColumn(TB 的别名) 20 | func (columnName ColumnName[TYPE]) TC(tab utils.GormTableNameFace) *TableColumn[TYPE] { 21 | return columnName.WithTable(tab) 22 | } 23 | 24 | // TN creates a TableColumn through associating this column with a table name string 25 | // TN 通过将此列与表名字符串关联创建 TableColumn 26 | func (columnName ColumnName[TYPE]) TN(tableName string) *TableColumn[TYPE] { 27 | return columnName.WithTable(utils.NewTableNameImp(tableName)) 28 | } 29 | 30 | // WithTable associates the column with a table interface and returns a TableColumn 31 | // WithTable 将列与表接口关联并返回 TableColumn 32 | func (columnName ColumnName[TYPE]) WithTable(tab utils.GormTableNameFace) *TableColumn[TYPE] { 33 | return &TableColumn[TYPE]{ 34 | tab: tab, 35 | cnm: columnName, 36 | } 37 | } 38 | 39 | // WithTableName associates the column with a table name string and returns a TableColumn 40 | // WithTableName 将列与表名字符串关联并返回 TableColumn 41 | func (columnName ColumnName[TYPE]) WithTableName(tableName string) *TableColumn[TYPE] { 42 | return columnName.WithTable(utils.NewTableNameImp(tableName)) 43 | } 44 | 45 | // TableColumn represents a combination of a table and a column. 46 | // TableColumn 表示表和列的组合。 47 | type TableColumn[TYPE any] struct { 48 | tab utils.GormTableNameFace 49 | cnm ColumnName[TYPE] 50 | } 51 | 52 | // Eq generates an equivalence condition in SQL format, ensuring type uniformity between two columns. 53 | // Eq 生成 SQL 格式的相等条件,确保两列之间的类型一致。 54 | func (tc *TableColumn[TYPE]) Eq(xc *TableColumn[TYPE]) string { 55 | return tc.Name() + " = " + xc.Name() 56 | } 57 | 58 | // Ne generates a SQL non-equivalence condition, ensuring type uniformity between two columns. 59 | // Ne 生成 SQL 格式的不等条件,确保两列之间的类型一致。 60 | func (tc *TableColumn[TYPE]) Ne(xc *TableColumn[TYPE]) string { 61 | return tc.Name() + " != " + xc.Name() 62 | } 63 | 64 | // Op generates a custom SQL operation between two columns using the specified operator. 65 | // Op 使用指定的 operand 生成两列之间的自定义 SQL 操作。 66 | func (tc *TableColumn[TYPE]) Op(op string, xc *TableColumn[TYPE]) string { 67 | return tc.Name() + " " + op + " " + xc.Name() 68 | } 69 | 70 | // Name returns the complete name of the column in the format "table.column". 71 | // Name 返回列的完全限定名称,格式为 "table.column"。 72 | func (tc *TableColumn[TYPE]) Name() string { 73 | return tc.tab.TableName() + "." + tc.cnm.Name() 74 | } 75 | 76 | // ColumnName retrieves the column name in a ColumnName format, representing the combination of the table and column. 77 | // ColumnName 获取以 ColumnName 格式表示的列名,代表表和列的组合。 78 | func (tc *TableColumn[TYPE]) ColumnName() ColumnName[TYPE] { 79 | return ColumnName[TYPE](tc.Name()) 80 | } 81 | 82 | // Cnm retrieves the column name in a ColumnName format, representing the combination of the table and column. 83 | // Cnm 获取以 ColumnName 格式表示的列名,代表表和列的组合。 84 | func (tc *TableColumn[TYPE]) Cnm() ColumnName[TYPE] { 85 | return ColumnName[TYPE](tc.Name()) 86 | } 87 | 88 | // Ob creates an OrderByBottle object to specify ordering based on the column name and direction. 89 | // Ob 基于列名和方向创建一个 OrderByBottle 对象用于指定排序。 90 | func (tc *TableColumn[TYPE]) Ob(direction string) OrderByBottle { 91 | return tc.Cnm().Ob(direction) 92 | } 93 | 94 | // AsAlias generates a SQL alias to the column in the format "table.column AS alias". 95 | // AsAlias 生成列的 SQL 别名,格式为 "table.column AS alias"。 96 | func (tc *TableColumn[TYPE]) AsAlias(alias string) string { 97 | return utils.ApplyAliasToColumn(tc.Name(), alias) 98 | } 99 | 100 | // AsName generates a SQL alias for the column using another ColumnName as the alias. 101 | // AsName 使用另一个 ColumnName 作为别名生成列的 SQL 别名。 102 | func (tc *TableColumn[TYPE]) AsName(alias ColumnName[TYPE]) string { 103 | return utils.ApplyAliasToColumn(tc.Name(), alias.Name()) 104 | } 105 | -------------------------------------------------------------------------------- /internal/examples/example4/example4_test.go: -------------------------------------------------------------------------------- 1 | package example4 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/yyle88/done" 8 | "github.com/yyle88/gormcnm" 9 | "github.com/yyle88/gormcnm/gormcnmstub" 10 | "github.com/yyle88/gormcnm/internal/tests" 11 | "github.com/yyle88/neatjson/neatjsons" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type UserOrder struct { 16 | UserID uint 17 | UserName string 18 | OrderID uint 19 | OrderAmount float64 20 | } 21 | 22 | func TestExample(t *testing.T) { 23 | tests.NewDBRun(t, func(db *gorm.DB) { 24 | done.Done(db.AutoMigrate(&User{}, &Order{})) 25 | 26 | users := []User{ 27 | {ID: 1, Name: "Alice"}, 28 | {ID: 2, Name: "Bob"}, 29 | } 30 | orders := []Order{ 31 | {ID: 1, UserID: 1, Amount: 100}, 32 | {ID: 2, UserID: 1, Amount: 200}, 33 | {ID: 3, UserID: 2, Amount: 300}, 34 | } 35 | done.Done(db.Create(&users).Error) 36 | done.Done(db.Create(&orders).Error) 37 | 38 | expectedText := neatjsons.S(selectFunc(t, db)) 39 | //使用第一种方案,确保两者结果相同 40 | require.Equal(t, expectedText, neatjsons.S(selectFunc1(t, db))) 41 | //使用第二种方案,确保两者结果相同 42 | require.Equal(t, expectedText, neatjsons.S(selectFunc2(t, db))) 43 | //使用第三种方案,确保两者结果相同 44 | require.Equal(t, expectedText, neatjsons.S(selectFunc3(t, db))) 45 | }) 46 | } 47 | 48 | // 这是比较常规的逻辑 49 | func selectFunc(t *testing.T, db *gorm.DB) []*UserOrder { 50 | var results []*UserOrder 51 | require.NoError(t, db.Table("users"). 52 | Select("users.id as user_id, users.name as user_name, orders.id as order_id, orders.amount as order_amount"). 53 | Joins("left join orders on orders.user_id = users.id"). 54 | Order("users.id asc, orders.id asc"). 55 | Scan(&results).Error) 56 | t.Log(neatjsons.S(results)) 57 | return results 58 | } 59 | 60 | // 这是使用名称的逻辑 61 | func selectFunc1(t *testing.T, db *gorm.DB) []*UserOrder { 62 | user := &User{} 63 | userColumns := user.TableColumns(gormcnm.NewTableDecoration(user.TableName())) 64 | order := &Order{} 65 | orderColumns := order.TableColumns(gormcnm.NewTableDecoration(order.TableName())) 66 | 67 | var results []*UserOrder 68 | require.NoError(t, db.Table(user.TableName()). 69 | Select(gormcnmstub.MergeStmts( 70 | userColumns.ID.AsAlias("user_id"), 71 | userColumns.Name.AsAlias("user_name"), 72 | orderColumns.ID.AsAlias("order_id"), 73 | orderColumns.Amount.AsAlias("order_amount"), 74 | )). 75 | Joins(userColumns.LEFTJOIN(order.TableName()).On(orderColumns.UserID.OnEq(userColumns.ID))). 76 | Order(userColumns.ID.Ob("asc").Ob(orderColumns.ID.Ob("asc")).Ox()). 77 | Scan(&results).Error) 78 | t.Log(neatjsons.S(results)) 79 | return results 80 | } 81 | 82 | // 这是使用名称的逻辑 83 | func selectFunc2(t *testing.T, db *gorm.DB) []*UserOrder { 84 | user := &User{} 85 | userColumns := user.TableColumns(gormcnm.NewTableDecoration(user.TableName())) 86 | order := &Order{} 87 | orderColumns := order.TableColumns(gormcnm.NewTableDecoration(order.TableName())) 88 | 89 | //这是使用名称的逻辑 90 | var results []*UserOrder 91 | require.NoError(t, db.Table(user.TableName()). 92 | Select(gormcnmstub.MergeStmts( 93 | userColumns.ID.AsName(gormcnm.New[uint]("user_id")), 94 | userColumns.Name.AsName("user_name"), 95 | orderColumns.ID.AsName(gormcnm.New[uint]("order_id")), 96 | orderColumns.Amount.AsName("order_amount"), 97 | )). 98 | Joins(userColumns.LEFTJOIN(order.TableName()).On(orderColumns.UserID.OnEq(userColumns.ID))). 99 | Order(userColumns.ID.Ob("asc").Ob(orderColumns.ID.Ob("asc")).Ox()). 100 | Scan(&results).Error) 101 | t.Log(neatjsons.S(results)) 102 | return results 103 | } 104 | 105 | func selectFunc3(t *testing.T, db *gorm.DB) []*UserOrder { 106 | user := &User{} 107 | userColumns := user.TableColumns(gormcnm.NewTableDecoration(user.TableName())) 108 | order := &Order{} 109 | orderColumns := order.TableColumns(gormcnm.NewTableDecoration(order.TableName())) 110 | 111 | userOrder := &UserOrder{} 112 | 113 | //这是使用名称的逻辑 114 | var results []*UserOrder 115 | require.NoError(t, db.Table(user.TableName()). 116 | Select(gormcnmstub.MergeStmts( 117 | userColumns.ID.AsName(gormcnm.Cnm(userOrder.UserID, "user_id")), 118 | userColumns.Name.AsName(gormcnm.Cnm(userOrder.UserName, "user_name")), 119 | orderColumns.ID.AsName(gormcnm.Cnm(userOrder.OrderID, "order_id")), 120 | orderColumns.Amount.AsName(gormcnm.Cnm(userOrder.OrderAmount, "order_amount")), 121 | )). 122 | Joins(userColumns.LEFTJOIN(order.TableName()).On(orderColumns.UserID.OnEq(userColumns.ID))). 123 | Order(userColumns.ID.Ob("asc").Ob(orderColumns.ID.Ob("asc")).Ox()). 124 | Scan(&results).Error) 125 | t.Log(neatjsons.S(results)) 126 | return results 127 | } 128 | -------------------------------------------------------------------------------- /qx_conjunction.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides conjunction operations with statement and arguments binding 2 | // Auto handles complex WHERE clauses with argument binding and boolean operators 3 | // Supports building dynamic queries with type-safe argument management and GORM integration 4 | // 5 | // gormcnm 提供查询连接词操作,具备语句和参数绑定功能 6 | // 自动处理复杂的 WHERE 子句,包含参数绑定和逻辑运算符 7 | // 支持构建动态查询,具备类型安全的参数管理和 GORM 集成 8 | package gormcnm 9 | 10 | import "gorm.io/gorm" 11 | 12 | // QxType is an alias when using QxConjunction as a short type name 13 | // QxType 是 QxConjunction 的别名,用作简短的类型名称 14 | type QxType = QxConjunction 15 | 16 | // NewQx creates a new QxConjunction instance with the provided statement and arguments 17 | // NewQx 使用提供的语句和参数创建一个新的 QxConjunction 实例 18 | func NewQx(stmt string, args ...interface{}) *QxType { 19 | return &QxType{ 20 | statementArgumentsTuple: newStatementArgumentsTuple(stmt, args), 21 | } 22 | } 23 | 24 | // QxConjunction is used to construct WHERE queries (AND, OR, NOT) and associated arguments. 25 | // QxConjunction 用于构造关系查询语句(AND、OR、NOT)以及其对应的参数,以供 db.Where 使用。 26 | // Example: When combining conditions like a.Eq("xyz") and b.Eq("uvw"), this class concatenates statements with (---) AND (---) and merges arguments into a new list. 27 | // 示例:当需要组合条件如 a.Eq("xyz") 和 b.Eq("uvw") 时,该工具类将语句用 (---) AND (---) 连接,并将参数列表合并为新的列表。 28 | type QxConjunction struct { 29 | *statementArgumentsTuple 30 | } 31 | 32 | // NewQxConjunction creates a new instance of QxConjunction with the provided statement and arguments. 33 | // NewQxConjunction 使用提供的语句和参数创建一个新的 QxConjunction 实例。 34 | func NewQxConjunction(stmt string, args ...interface{}) *QxConjunction { 35 | return &QxConjunction{ 36 | statementArgumentsTuple: newStatementArgumentsTuple(stmt, args), 37 | } 38 | } 39 | 40 | // Qx is shorthand for creating a new QxConjunction instance. 41 | // Qx 是创建 QxConjunction 实例的简写形式。 42 | func Qx(stmt string, args ...interface{}) *QxConjunction { 43 | return NewQxConjunction(stmt, args...) 44 | } 45 | 46 | // AND combines the current QxConjunction instance with multiple QxConjunction instances using "AND". 47 | // AND 使用 "AND" 将当前 QxConjunction 实例与多个 QxConjunction 实例组合在一起。 48 | func (qx *QxConjunction) AND(cs ...*QxConjunction) *QxConjunction { 49 | var qss []QsConjunction 50 | var qas []*statementArgumentsTuple 51 | for _, c := range cs { 52 | qss = append(qss, QsConjunction(c.stmt)) 53 | qas = append(qas, c.statementArgumentsTuple) 54 | } 55 | 56 | return &QxConjunction{ 57 | statementArgumentsTuple: newStatementArgumentsTuple(string(QsConjunction(qx.stmt).AND(qss...)), qx.safeCombineArguments(qas)), 58 | } 59 | } 60 | 61 | // OR combines the current QxConjunction instance with multiple QxConjunction instances using "OR". 62 | // OR 使用 "OR" 将当前 QxConjunction 实例与多个 QxConjunction 实例组合在一起。 63 | func (qx *QxConjunction) OR(cs ...*QxConjunction) *QxConjunction { 64 | var qss []QsConjunction 65 | var qas []*statementArgumentsTuple 66 | for _, c := range cs { 67 | qss = append(qss, QsConjunction(c.stmt)) 68 | qas = append(qas, c.statementArgumentsTuple) 69 | } 70 | return &QxConjunction{ 71 | statementArgumentsTuple: newStatementArgumentsTuple(string(QsConjunction(qx.stmt).OR(qss...)), qx.safeCombineArguments(qas)), 72 | } 73 | } 74 | 75 | // NOT negates the current QxConjunction instance by wrapping the statement with "NOT". 76 | // NOT 通过在语句外包裹 "NOT" 来对当前 QxConjunction 实例进行逻辑取反。 77 | func (qx *QxConjunction) NOT() *QxConjunction { 78 | return &QxConjunction{ 79 | statementArgumentsTuple: newStatementArgumentsTuple(string(QsConjunction(qx.stmt).NOT()), qx.args), 80 | } 81 | } 82 | 83 | // AND1 creates a new QxConjunction instance with the given statement and arguments, then combines it with the current instance using "AND". 84 | // AND1 使用给定的语句和参数创建一个新的 QxConjunction 实例,然后使用 "AND" 将其与当前实例组合。 85 | func (qx *QxConjunction) AND1(stmt string, args ...interface{}) *QxConjunction { 86 | return qx.AND(NewQxConjunction(stmt, args...)) 87 | } 88 | 89 | // OR1 creates a new QxConjunction instance with the given statement and arguments, then combines it with the current instance using "OR". 90 | // OR1 使用给定的语句和参数创建一个新的 QxConjunction 实例,然后使用 "OR" 将其与当前实例组合。 91 | func (qx *QxConjunction) OR1(stmt string, args ...interface{}) *QxConjunction { 92 | return qx.OR(NewQxConjunction(stmt, args...)) 93 | } 94 | 95 | // Scope converts the QxConjunction to a GORM ScopeFunction used with db.Scopes(). 96 | // It applies the SELECT conditions defined by QxConjunction to the GORM select. 97 | // Scope 将 QxConjunction 转换为 GORM 的 ScopeFunction,以便于被 db.Scopes() 调用。 98 | // 它将 QxConjunction 定义的查询条件应用于 GORM 查询。 99 | func (qx *QxConjunction) Scope() ScopeFunction { 100 | return func(db *gorm.DB) *gorm.DB { 101 | return db.Where(qx.Qs(), qx.args...) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cname_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests demonstrate the core functions of type-safe column operations 2 | // Auto validates ColumnName operations with GORM integration in an SQLite memory-based database 3 | // Tests examine basic operations, comparisons, and SQL where generation 4 | // 5 | // gormcnm 测试包演示了类型安全列操作的核心功能 6 | // 自动验证 ColumnName 操作与 GORM 在 SQLite 内存数据库中的集成 7 | // 测试涵盖基础操作、比较和 SQL 查询生成 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestColumnName_Op(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Type string `gorm:"column:type;"` 23 | } 24 | 25 | const columnName = ColumnName[string]("name") 26 | 27 | tests.NewDBRun(t, func(db *gorm.DB) { 28 | require.NoError(t, db.AutoMigrate(&Example{})) 29 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 30 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 31 | 32 | { 33 | var one Example 34 | require.NoError(t, db.Where(columnName.Op("=?", "abc")).First(&one).Error) 35 | require.Equal(t, "abc", one.Name) 36 | t.Log(neatjsons.S(one)) 37 | } 38 | { 39 | var one Example 40 | require.NoError(t, db.Where(columnName.Eq("abc")).First(&one).Error) 41 | require.Equal(t, "abc", one.Name) 42 | t.Log(neatjsons.S(one)) 43 | } 44 | { 45 | var one Example 46 | require.NoError(t, db.Where(columnName.BetweenAND("aba", "abd")).First(&one).Error) 47 | require.Equal(t, "abc", one.Name) 48 | t.Log(neatjsons.S(one)) 49 | } 50 | { 51 | var one Example 52 | require.NoError(t, db.Where(columnName.Between("aba", "abd")).First(&one).Error) 53 | require.Equal(t, "abc", one.Name) 54 | t.Log(neatjsons.S(one)) 55 | } 56 | { 57 | var one Example 58 | require.NoError(t, db.Where(columnName.NotBetween("aca", "azz")).First(&one).Error) 59 | require.Equal(t, "aaa", one.Name) 60 | t.Log(neatjsons.S(one)) 61 | } 62 | { 63 | var one Example 64 | require.ErrorIs(t, gorm.ErrRecordNotFound, db.Where(columnName.IsNULL()).First(&one).Error) 65 | require.Equal(t, "", one.Name) 66 | t.Log(neatjsons.S(one)) 67 | } 68 | { 69 | var one Example 70 | require.NoError(t, db.Where(columnName.IsNotNULL()).First(&one).Error) 71 | require.Contains(t, []string{"abc", "aaa"}, one.Name) 72 | t.Log(neatjsons.S(one)) 73 | } 74 | { 75 | var res []*Example 76 | require.NoError(t, db.Where(columnName.In([]string{"abc", "aaa"})).Find(&res).Error) 77 | require.Contains(t, []string{"abc", "aaa"}, res[0].Name) 78 | require.Contains(t, []string{"abc", "aaa"}, res[1].Name) 79 | t.Log(neatjsons.S(res)) 80 | } 81 | { 82 | var res []*Example 83 | require.NoError(t, db.Where(columnName.NotIn([]string{"aaa", "bbb"})).Find(&res).Error) 84 | for _, v := range res { 85 | require.NotEqual(t, "aaa", v.Name) 86 | require.NotEqual(t, "bbb", v.Name) 87 | } 88 | t.Log(neatjsons.S(res)) 89 | } 90 | }) 91 | } 92 | 93 | func TestColumnName_Op2(t *testing.T) { 94 | type Example struct { 95 | Name string `gorm:"primary_key;type:varchar(100);"` 96 | Type string `gorm:"column:type;"` 97 | } 98 | 99 | const ( 100 | columnName = ColumnName[string]("name") 101 | columnType = ColumnName[string]("type") 102 | ) 103 | 104 | tests.NewDBRun(t, func(db *gorm.DB) { 105 | require.NoError(t, db.AutoMigrate(&Example{})) 106 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 107 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 108 | 109 | var one Example 110 | require.NoError(t, db.Where(columnName.Qs("=?")+" AND "+columnType.Qs("=?"), "abc", "xyz").First(&one).Error) 111 | require.Equal(t, "abc", one.Name) 112 | t.Log(neatjsons.S(one)) 113 | }) 114 | } 115 | 116 | func TestColumnName_Op3(t *testing.T) { 117 | type Example struct { 118 | Name string `gorm:"primary_key;type:varchar(100);"` 119 | Type string `gorm:"column:type;"` 120 | } 121 | 122 | const columnName = ColumnName[string]("name") 123 | 124 | tests.NewDBRun(t, func(db *gorm.DB) { 125 | require.NoError(t, db.AutoMigrate(&Example{})) 126 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 127 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 128 | 129 | { 130 | var one Example 131 | require.NoError(t, db.Where(columnName.Like("%b%")).First(&one).Error) 132 | require.Equal(t, "abc", one.Name) 133 | t.Log(neatjsons.S(one)) 134 | } 135 | { 136 | var one Example 137 | require.NoError(t, db.Where(columnName.NotLike("%b%")).First(&one).Error) 138 | require.Equal(t, "aaa", one.Name) 139 | t.Log(neatjsons.S(one)) 140 | } 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /internal/examples/example3/example3_test.go: -------------------------------------------------------------------------------- 1 | // Package example3 demonstrates advanced table join operations using gormcnm 2 | // Auto generates complex SQL joins with type-safe column operations 3 | // Tests compare traditional SQL with gormcnm-generated equivalent queries 4 | // 5 | // example3 包演示了使用 gormcnm 的高级表连接操作 6 | // 自动生成具有类型安全列操作的复杂 SQL 连接 7 | // 测试比较传统 SQL 与 gormcnm 生成的等效查询 8 | package example3 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/done" 15 | "github.com/yyle88/gormcnm" 16 | "github.com/yyle88/gormcnm/gormcnmstub" 17 | "github.com/yyle88/gormcnm/internal/tests" 18 | "github.com/yyle88/neatjson/neatjsons" 19 | "gorm.io/gorm" 20 | ) 21 | 22 | type UserOrder struct { 23 | UserID uint 24 | UserName string 25 | OrderID uint 26 | OrderAmount float64 27 | } 28 | 29 | func TestExample(t *testing.T) { 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | done.Done(db.AutoMigrate(&User{}, &Order{})) 32 | 33 | users := []User{ 34 | {ID: 1, Name: "Alice"}, 35 | {ID: 2, Name: "Bob"}, 36 | } 37 | orders := []Order{ 38 | {ID: 1, UserID: 1, Amount: 100}, 39 | {ID: 2, UserID: 1, Amount: 200}, 40 | {ID: 3, UserID: 2, Amount: 300}, 41 | } 42 | done.Done(db.Create(&users).Error) 43 | done.Done(db.Create(&orders).Error) 44 | 45 | expectedText := neatjsons.S(selectFunc(t, db)) 46 | //使用第一种方案,确保两者结果相同 47 | require.Equal(t, expectedText, neatjsons.S(selectFunc1(t, db))) 48 | //使用第二种方案,确保两者结果相同 49 | require.Equal(t, expectedText, neatjsons.S(selectFunc2(t, db))) 50 | //使用第三种方案,确保两者结果相同 51 | require.Equal(t, expectedText, neatjsons.S(selectFunc3(t, db))) 52 | }) 53 | } 54 | 55 | // 这是比较常规的逻辑 56 | func selectFunc(t *testing.T, db *gorm.DB) []*UserOrder { 57 | var results []*UserOrder 58 | require.NoError(t, db.Table("users"). 59 | Select("users.id as user_id, users.name as user_name, orders.id as order_id, orders.amount as order_amount"). 60 | Joins("left join orders on orders.user_id = users.id"). 61 | Order("users.id asc, orders.id asc"). 62 | Scan(&results).Error) 63 | t.Log(neatjsons.S(results)) 64 | return results 65 | } 66 | 67 | // 这是使用名称的逻辑 68 | func selectFunc1(t *testing.T, db *gorm.DB) []*UserOrder { 69 | user := &User{} 70 | userColumns := user.Columns() 71 | order := &Order{} 72 | orderColumns := order.Columns() 73 | 74 | var results []*UserOrder 75 | require.NoError(t, db.Table(user.TableName()). 76 | Select(gormcnmstub.MergeStmts( 77 | userColumns.ID.TB(user).AsAlias("user_id"), 78 | userColumns.Name.TB(user).AsAlias("user_name"), 79 | orderColumns.ID.TB(order).AsAlias("order_id"), 80 | orderColumns.Amount.TB(order).AsAlias("order_amount"), 81 | )). 82 | Joins(userColumns.LEFTJOIN(order.TableName()).On(orderColumns.UserID.TB(order).Eq(userColumns.ID.TB(user)))). 83 | Order(userColumns.ID.TB(user).Ob("asc").Ob(orderColumns.ID.TB(order).Ob("asc")).Ox()). 84 | Scan(&results).Error) 85 | t.Log(neatjsons.S(results)) 86 | return results 87 | } 88 | 89 | // 这是使用名称的逻辑 90 | func selectFunc2(t *testing.T, db *gorm.DB) []*UserOrder { 91 | user := &User{} 92 | userColumns := user.Columns() 93 | order := &Order{} 94 | orderColumns := order.Columns() 95 | 96 | //这是使用名称的逻辑 97 | var results []*UserOrder 98 | require.NoError(t, db.Table(user.TableName()). 99 | Select(gormcnmstub.MergeStmts( 100 | userColumns.ID.WithTable(user).AsName(gormcnm.New[uint]("user_id")), 101 | userColumns.Name.WithTable(user).AsName("user_name"), 102 | orderColumns.ID.WithTable(order).AsName(gormcnm.New[uint]("order_id")), 103 | orderColumns.Amount.WithTable(order).AsName("order_amount"), 104 | )). 105 | Joins(userColumns.LEFTJOIN(order.TableName()).On(orderColumns.UserID.WithTable(order).Eq(userColumns.ID.WithTable(user)))). 106 | Order(userColumns.ID.WithTable(user).Ob("asc").Ob(orderColumns.ID.WithTable(order).Ob("asc")).Ox()). 107 | Scan(&results).Error) 108 | t.Log(neatjsons.S(results)) 109 | return results 110 | } 111 | 112 | func selectFunc3(t *testing.T, db *gorm.DB) []*UserOrder { 113 | user := &User{} 114 | userColumns := user.Columns() 115 | order := &Order{} 116 | orderColumns := order.Columns() 117 | 118 | userOrder := &UserOrder{} 119 | 120 | //这是使用名称的逻辑 121 | var results []*UserOrder 122 | require.NoError(t, db.Table(user.TableName()). 123 | Select(gormcnmstub.MergeStmts( 124 | userColumns.ID.TC(user).AsName(gormcnm.Cnm(userOrder.UserID, "user_id")), 125 | userColumns.Name.TC(user).AsName(gormcnm.Cnm(userOrder.UserName, "user_name")), 126 | orderColumns.ID.TC(order).AsName(gormcnm.Cnm(userOrder.OrderID, "order_id")), 127 | orderColumns.Amount.TC(order).AsName(gormcnm.Cnm(userOrder.OrderAmount, "order_amount")), 128 | )). 129 | Joins(userColumns.LEFTJOIN(order.TableName()).On(orderColumns.UserID.TC(order).Eq(userColumns.ID.TC(user)))). 130 | Order(userColumns.ID.TC(user).Ob("asc").Ob(orderColumns.ID.TC(order).Ob("asc")).Ox()). 131 | Scan(&results).Error) 132 | t.Log(neatjsons.S(results)) 133 | return results 134 | } 135 | -------------------------------------------------------------------------------- /gorm_scope_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate GORM scope function operations and integration 2 | // Auto verifies ScopeFunction type operations with GORM db.Scopes() method 3 | // Tests examine scope composition, reusable where modifiers, and database integration 4 | // 5 | // gormcnm 测试包验证 GORM 作用域函数操作和集成 6 | // 自动验证 ScopeFunction 类型功能与 GORM db.Scopes() 方法 7 | // 测试涵盖作用域组合、可重用查询修饰符和数据库集成 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestOrderByBottle_Scope(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Type string `gorm:"column:type;"` 23 | } 24 | 25 | const ( 26 | columnName = ColumnName[string]("name") 27 | columnType = ColumnName[string]("type") 28 | ) 29 | 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | require.NoError(t, db.AutoMigrate(&Example{})) 32 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 33 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 34 | 35 | { 36 | var res []*Example 37 | require.NoError(t, db.Where(columnName.In([]string{"abc", "aaa"})). 38 | Scopes(columnName.OrderByBottle("asc"). 39 | Ob(columnType.Ob("desc")). 40 | Scope()). 41 | Find(&res).Error) 42 | require.Equal(t, "aaa", res[0].Name) 43 | require.Equal(t, "abc", res[1].Name) 44 | t.Log(neatjsons.S(res)) 45 | } 46 | { 47 | var res []*Example 48 | require.NoError(t, db.Where(columnName.In([]string{"abc", "aaa"})). 49 | Scopes(columnName.Ob("desc"). 50 | OrderByBottle(columnType.OrderByBottle("asc")). 51 | Scope()). 52 | Find(&res).Error) 53 | require.Equal(t, "abc", res[0].Name) 54 | require.Equal(t, "aaa", res[1].Name) 55 | t.Log(neatjsons.S(res)) 56 | } 57 | }) 58 | } 59 | 60 | func TestQxConjunction_Scope(t *testing.T) { 61 | type Example struct { 62 | Name string `gorm:"primary_key;type:varchar(100);"` 63 | Type string `gorm:"column:type;"` 64 | } 65 | 66 | const ( 67 | columnName = ColumnName[string]("name") 68 | columnType = ColumnName[string]("type") 69 | ) 70 | 71 | tests.NewDBRun(t, func(db *gorm.DB) { 72 | require.NoError(t, db.AutoMigrate(&Example{})) 73 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 74 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 75 | 76 | { 77 | var one Example 78 | require.NoError(t, db. 79 | Scopes(Qx(columnName.BetweenAND("aba", "abd")). 80 | AND(Qx(columnType.IsNotNULL())). 81 | Scope()). 82 | First(&one).Error) 83 | require.Equal(t, "abc", one.Name) 84 | t.Log(neatjsons.S(one)) 85 | } 86 | }) 87 | } 88 | 89 | func TestQxConjunction_Scope_Example2(t *testing.T) { 90 | type Example struct { 91 | Name string `gorm:"primary_key;type:varchar(100);"` 92 | Type string `gorm:"column:type;"` 93 | Rank int `gorm:"column:rank;"` 94 | } 95 | 96 | const ( 97 | columnType = ColumnName[string]("type") 98 | columnRank = ColumnName[int]("rank") 99 | ) 100 | 101 | tests.NewDBRun(t, func(db *gorm.DB) { 102 | require.NoError(t, db.AutoMigrate(&Example{})) 103 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz", Rank: 25}).Error) 104 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 30}).Error) 105 | require.NoError(t, db.Save(&Example{Name: "def", Type: "yyy", Rank: 20}).Error) 106 | 107 | { 108 | var one Example 109 | require.NoError(t, db. 110 | Scopes(NewQx(columnRank.Gt(22)). 111 | AND(Qx(columnType.Ne("xxx"))). 112 | Scope()). 113 | First(&one).Error) 114 | require.Equal(t, "abc", one.Name) 115 | t.Log(neatjsons.S(one)) 116 | } 117 | }) 118 | } 119 | 120 | func TestSelectStatement_Scope(t *testing.T) { 121 | type Example struct { 122 | Name string `gorm:"primary_key;type:varchar(100);"` 123 | Rank int `gorm:"column:rank;"` 124 | } 125 | 126 | const ( 127 | columnName = ColumnName[string]("name") 128 | columnRank = ColumnName[int]("rank") 129 | ) 130 | 131 | tests.NewDBRun(t, func(db *gorm.DB) { 132 | require.NoError(t, db.AutoMigrate(&Example{})) 133 | require.NoError(t, db.Save(&Example{Name: "abc", Rank: 100}).Error) 134 | require.NoError(t, db.Save(&Example{Name: "aaa", Rank: 101}).Error) 135 | 136 | type Result struct { 137 | Name string 138 | Mark int 139 | } 140 | 141 | var results []*Result 142 | //这是因为 rank 是您查询中的实际字段,而 mark 是 rank 字段的别名。在 ORDER BY 子句中,您应该使用实际的列名(即 rank),而不是别名(即 mark)。 143 | require.NoError(t, db.Model(&Example{}). 144 | Scopes(NewSx(columnName.Name()). 145 | Combine(NewSx(columnRank.AsAlias("mark"))). 146 | Scope()). 147 | Order(columnRank.Ob("desc").Ox()).Find(&results).Error) 148 | t.Log(neatjsons.S(results)) 149 | 150 | require.Len(t, results, 2) 151 | require.Equal(t, results[0].Name, "aaa") 152 | require.Equal(t, results[0].Mark, 101) 153 | require.Equal(t, results[1].Name, "abc") 154 | require.Equal(t, results[1].Mark, 100) 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /gormcnmjson/json.go: -------------------------------------------------------------------------------- 1 | // Package gormcnmjson enables type-safe JSON column operations within GORM 2 | // Supports SQLite JSON functions with compile-time type checking 3 | // Works with both string and []byte JSON column types 4 | // 5 | // gormcnmjson 为 GORM 提供类型安全的 JSON 列操作 6 | // 支持 SQLite JSON 函数并提供编译时类型检查 7 | // 同时支持 string 和 []byte 类型的 JSON 列 8 | package gormcnmjson 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/yyle88/gormcnm" 14 | ) 15 | 16 | // Column represents a JSON column with type-safe SQL operations 17 | // Provides methods to generate JSON-specific SQL expressions 18 | // 19 | // Column 表示一个 JSON 列,提供类型安全的 SQL 操作 20 | // 提供生成 JSON 特定 SQL 表达式的方法 21 | type Column struct { 22 | name string // Column name in database // 数据库中的列名 23 | } 24 | 25 | // New creates a Column from a ColumnName with generic type support 26 | // Accepts both string and []byte column types with versatile options 27 | // 28 | // New 从 ColumnName 创建 Column,支持泛型类型 29 | // 接受 string 和 []byte 列类型,提供多种选择 30 | func New[T ~string | ~[]byte](columnName gormcnm.ColumnName[T]) Column { 31 | return Column{name: columnName.Name()} 32 | } 33 | 34 | // Raw creates a Column from a []byte-based ColumnName 35 | // Dedicated creation function to handle datatypes.JSON and related []byte types 36 | // 37 | // Raw 从基于 []byte 的 ColumnName 创建 Column 38 | // 专门用于 datatypes.JSON 和相关的 []byte 类型 39 | func Raw[T ~[]byte](columnName gormcnm.ColumnName[T]) Column { 40 | return Column{name: columnName.Name()} 41 | } 42 | 43 | // Name returns the underlying column name as a string 44 | // Use this when you need the raw column name in SQL expressions 45 | // 46 | // Name 返回底层列名字符串 47 | // 当需要在 SQL 表达式中使用原始列名时使用此方法 48 | func (co Column) Name() string { 49 | return co.name 50 | } 51 | 52 | // Get extracts a JSON value as text using the ->> operation 53 | // Returns a type-safe string ColumnName to allow chaining conditions 54 | // 55 | // Get 使用 ->> 操作将 JSON 值提取为文本 56 | // 返回类型安全的字符串 ColumnName 用于链式条件 57 | func (co Column) Get(path string) gormcnm.ColumnName[string] { 58 | return gormcnm.ColumnName[string]( 59 | fmt.Sprintf("%s ->> '$.%s'", co.name, path), 60 | ) 61 | } 62 | 63 | // Extract extracts a JSON sub-object using the -> operation 64 | // Returns a Column that supports nested operations 65 | // 66 | // Extract 使用 -> 操作提取 JSON 子对象 67 | // 返回 Column 用于额外的嵌套操作 68 | func (co Column) Extract(path string) Column { 69 | return Column{ 70 | name: fmt.Sprintf("%s -> '$.%s'", co.name, path), 71 | } 72 | } 73 | 74 | // GetInt extracts a JSON value as an int with type casting 75 | // Returns a type-safe int ColumnName to use in numeric comparisons 76 | // 77 | // GetInt 将 JSON 值提取为整数并进行类型转换 78 | // 返回类型安全的 int ColumnName 用于数值比较 79 | func (co Column) GetInt(path string) gormcnm.ColumnName[int] { 80 | return gormcnm.ColumnName[int]( 81 | fmt.Sprintf("CAST(%s ->> '$.%s' AS INTEGER)", co.name, path), 82 | ) 83 | } 84 | 85 | // Length returns the length of a JSON text/object using JSON_ARRAY_LENGTH 86 | // If path is blank, measures the root JSON; otherwise measures the nested path 87 | // 88 | // Length 使用 JSON_ARRAY_LENGTH 返回 JSON 数组的长度 89 | // 如果 path 为空则测量根 JSON,否则测量嵌套路径 90 | func (co Column) Length(path string) gormcnm.ColumnName[int] { 91 | if path == "" { 92 | return gormcnm.ColumnName[int]( 93 | fmt.Sprintf("JSON_ARRAY_LENGTH(%s)", co.name), 94 | ) 95 | } 96 | return gormcnm.ColumnName[int]( 97 | fmt.Sprintf("JSON_ARRAY_LENGTH(%s, '$.%s')", co.name, path), 98 | ) 99 | } 100 | 101 | // Type returns the JSON type of a value using JSON_TYPE function 102 | // If path is blank, checks root JSON type; otherwise checks the nested path 103 | // 104 | // Type 使用 JSON_TYPE 函数返回 JSON 值的类型 105 | // 如果 path 为空则检查根 JSON 类型,否则检查嵌套路径 106 | func (co Column) Type(path string) gormcnm.ColumnName[string] { 107 | if path == "" { 108 | return gormcnm.ColumnName[string]( 109 | fmt.Sprintf("JSON_TYPE(%s)", co.name), 110 | ) 111 | } 112 | return gormcnm.ColumnName[string]( 113 | fmt.Sprintf("JSON_TYPE(%s, '$.%s')", co.name, path), 114 | ) 115 | } 116 | 117 | // Valid checks if the JSON text has a valid format using JSON_VALID 118 | // Returns 1 if JSON is valid, 0 if JSON is invalid 119 | // 120 | // Valid 使用 JSON_VALID 检查 JSON 文本是否格式正确 121 | // 返回 1 表示有效的 JSON,返回 0 表示无效的 JSON 122 | func (co Column) Valid() gormcnm.ColumnName[int] { 123 | return gormcnm.ColumnName[int]( 124 | fmt.Sprintf("JSON_VALID(%s)", co.name), 125 | ) 126 | } 127 | 128 | // Set updates a JSON value at the specified path using JSON_SET 129 | // Returns a Column with the modified expression, intended to be used in UPDATE statements 130 | // 131 | // Set 使用 JSON_SET 在指定路径更新 JSON 值 132 | // 返回包含修改表达式的 Column 用于 UPDATE 语句 133 | func (co Column) Set(path string, value interface{}) Column { 134 | return Column{ 135 | name: fmt.Sprintf("JSON_SET(%s, '$.%s', '%v')", co.name, path, value), 136 | } 137 | } 138 | 139 | // Remove deletes a value at the specified path using JSON_REMOVE 140 | // Returns a Column with the delete expression, intended to be used in UPDATE statements 141 | // 142 | // Remove 使用 JSON_REMOVE 删除指定路径的值 143 | // 返回包含删除表达式的 Column 用于 UPDATE 语句 144 | func (co Column) Remove(path string) Column { 145 | return Column{ 146 | name: fmt.Sprintf("JSON_REMOVE(%s, '$.%s')", co.name, path), 147 | } 148 | } 149 | 150 | // AsAlias creates a column alias, intended to be used in SELECT statements 151 | // Returns the column expression with the specified alias name 152 | // 153 | // AsAlias 为 SELECT 语句创建列别名 154 | // 返回带有指定别名的列表达式 155 | func (co Column) AsAlias(alias string) string { 156 | return co.name + " as " + alias 157 | } 158 | -------------------------------------------------------------------------------- /internal/docs/README_OLD_DOC.en.md: -------------------------------------------------------------------------------- 1 | # `gormcnm` - Using GORM with Model Struct Fields Defined in Go for CRUD Operations 2 | 3 | ## Usage 4 | 5 | Assume your model definition looks like this: 6 | ```go 7 | type Example struct { 8 | Name string `gorm:"primary_key;type:varchar(100);"` 9 | Type string `gorm:"column:type;"` 10 | Rank int `gorm:"column:rank;"` 11 | } 12 | ``` 13 | It's clear that the column names for this model are `name`, `type`, and `rank`. When using GORM, you would typically query like this: 14 | ```go 15 | err := db.Where("name=?", "abc").First(&res).Error 16 | ``` 17 | Since `name` is a hardcoded field, we expect it to remain stable in the `Example` struct, and not be modified (e.g., deleted or type-changed) as the business evolves. This forms a development habit that requires us to be cautious when defining models because they are the foundation of all logic. 18 | 19 | However, "ensuring models are not modified" is actually an unrealistic expectation. [The background behind this idea](./CREATION_IDEAS.en.md) provides the context and motivation for solving this problem, though you can skip reading that. Let's now look at how to use `gormcnm`. 20 | 21 | ### Simple Demo: 22 | 23 | [Simple Demo](../demos/demo1x/main.go) | [Simple Demo](../demos/demo2x/main.go) 24 | 25 | These are the simplest examples, but they are at a demo level. 26 | 27 | We recommend looking at the demo in this project: [Automated Column Name Generation Tool for `gormcnm` - `gormcngen`](https://github.com/yyle88/gormcngen). Please refer to the README of this project to find the demo code, which will teach you how to use the tool more effectively. This is the core and truly useful part. 28 | 29 | ### Assume your model is like this: 30 | ```go 31 | type Example struct { 32 | Name string `gorm:"primary_key;type:varchar(100);"` 33 | Type string `gorm:"column:type;"` 34 | Rank int `gorm:"column:rank;"` 35 | } 36 | ``` 37 | 38 | The column definitions for this model are: 39 | ```go 40 | const ( 41 | columnName = gormcnm.ColumnName[string]("name") 42 | columnType = gormcnm.ColumnName[string]("type") 43 | columnRank = gormcnm.ColumnName[int]("rank") 44 | ) 45 | ``` 46 | 47 | Now, querying with GORM would look like this: 48 | ```go 49 | // SELECT * FROM `examples` WHERE name="abc" AND type="xyz" AND rank>100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 50 | var res Example 51 | if err := db.Where(columnName.Eq("abc")). 52 | Where(columnType.Eq("xyz")). 53 | Where(columnRank.Gt(100)). 54 | Where(columnRank.Lt(200)). 55 | First(&res).Error; err != nil { 56 | panic(errors.WithMessage(err, "wrong")) 57 | } 58 | fmt.Println(res) 59 | ``` 60 | This query avoids hardcoding column names in your code, ensuring that when refactoring, you won't forget to update the column names or set the wrong field types. 61 | 62 | For example, if `name` is a string and `rank` is an int, using: 63 | ```go 64 | db.Where("name=?", 1).First(&res).Error // This will cause an error because `name` is a string, and the query will definitely fail. 65 | ``` 66 | Typically, if you use: 67 | ```go 68 | db.Where("name=?", "abc").UpdateColumns(map[string]any{"rank":"xyz"}) // This will also cause an error because `rank` is an int, not a string. 69 | ``` 70 | Since `db.Where` requires an `interface{}` type argument and `UpdateColumns` uses a `map[string]any`, this mismatch won't be caught at compile time, leading to runtime errors. This makes it harder to refactor and detect issues quickly. 71 | 72 | Using this tool can help prevent such problems. 73 | 74 | ### Automatically Generating Column Names 75 | 76 | If you don’t want to manually write the constant list of column names, you can use the companion automation tool to generate column names for all your models. 77 | 78 | The tool will generate constant configuration code for column names. This tool project is called [Automated Column Name Generation package for *gormcnm* - *gormcngen*](https://github.com/yyle88/gormcngen). Just look at the README for details. 79 | 80 | The automatically generated column name definitions look like this (to avoid duplicate fields like `id`, `created_at`, `updated_at`, and `deleted_at`, we use a custom class to encapsulate them): 81 | ```go 82 | type ExampleColumns struct { 83 | Name gormcnm.ColumnName[string] 84 | Type gormcnm.ColumnName[string] 85 | Rank gormcnm.ColumnName[int] 86 | } 87 | 88 | func (*Example) Columns() *ExampleColumns { 89 | return &ExampleColumns{ 90 | Name: "name", 91 | Type: "type", 92 | Rank: "rank", 93 | } 94 | } 95 | ``` 96 | 97 | You can then use it like this: 98 | ```go 99 | // SELECT * FROM `examples` WHERE name="abc" AND type="xyz" AND rank>100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 100 | var res models.Example 101 | var cls = res.Columns() 102 | if err := db.Where(cls.Name.Eq("abc")). 103 | Where(cls.Type.Eq("xyz")). 104 | Where(cls.Rank.Gt(100)). 105 | Where(cls.Rank.Lt(200)). 106 | First(&res).Error; err != nil { 107 | panic(errors.WithMessage(err, "wrong")) // Just for demo 108 | } 109 | fmt.Println(res) 110 | ``` 111 | 112 | Other query logic is encapsulated as well, and you can gradually get familiar with how to use it in development. 113 | 114 | ## Installation 115 | 116 | To use this project, run: 117 | ```go 118 | go get github.com/yyle88/gormcnm 119 | ``` 120 | 121 | Then import it into your code: 122 | ```go 123 | import "github.com/yyle88/gormcnm" 124 | ``` 125 | 126 | ## Advantages 127 | 128 | There are many advantages, some of which are discussed in the development ideas document [Development Ideas](./CREATION_IDEAS.en.md). Here are some obvious benefits: 129 | 130 | 1. **Safe Refactoring**: It ensures safe refactoring, especially when renaming, modifying, or deleting fields. You can catch errors during static analysis. 131 | 2. **Convenient Refactoring**: For example, when changing `Name` to `Username`, you can use the IDE's rename feature (e.g., Shift+F6 in GoLand) to easily rename `Name` to `Username` in both the model and the `ExampleColumns.Name` constant. This ensures you won’t affect other tables, making refactoring more efficient. 132 | 3. **Faster Coding**: Writing `db.Where(cls.Name.Eq("abc"))` is more efficient than `db.Where("name=?", "abc")`. This advantage becomes more noticeable with longer field names (thanks to IDE code suggestions that help auto-complete after typing `cls.`). 133 | 4. **Easier Code Searching**: If you want to search for changes made to `name` in the database, you can search for references to `Name` in the model and find the code location in `db.Save` or `db.Create`. Searching for `cls.Name` will help you find relevant code locations in `db.Update`, `db.UpdateColumn`, or `db.Updates`. 134 | 135 | Give me stars. Thank you!!! 136 | -------------------------------------------------------------------------------- /qx_conjunction_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate conjunction operations with parameter binding 2 | // Auto verifies QxConjunction functionality with statement and arguments management 3 | // Tests cover AND, OR combinations, parameter binding, and GORM WHERE clause integration 4 | // 5 | // gormcnm 测试包验证查询连接词操作,具备参数绑定功能 6 | // 自动验证 QxConjunction 功能,包含语句和参数管理 7 | // 测试涵盖 AND、OR 组合、参数绑定和 GORM WHERE 子句集成 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestColumnQx_AND(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Type string `gorm:"column:type;"` 23 | } 24 | 25 | const ( 26 | columnName = ColumnName[string]("name") 27 | columnType = ColumnName[string]("type") 28 | ) 29 | 30 | tests.NewDBRun(t, func(db *gorm.DB) { 31 | require.NoError(t, db.AutoMigrate(&Example{})) 32 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 33 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 34 | 35 | { 36 | var one Example 37 | qx := columnName.Qx("=?", "abc").AND(columnType.Qx("=?", "xyz")) 38 | t.Log(qx.Qs()) 39 | t.Log(qx.Args()) 40 | require.NoError(t, db.Where(qx.Qs(), qx.Args()...).First(&one).Error) 41 | require.Equal(t, "abc", one.Name) 42 | t.Log(neatjsons.S(one)) 43 | } 44 | { 45 | var res []*Example 46 | qx := columnName.Qx("=?", "abc").OR(columnName.Qx("=?", "aaa")) 47 | t.Log(qx.Qs()) 48 | t.Log(qx.Args()) 49 | require.NoError(t, db.Where(qx.Qs(), qx.Args()...).Find(&res).Error) 50 | require.Contains(t, []string{"abc", "aaa"}, res[0].Name) 51 | require.Contains(t, []string{"abc", "aaa"}, res[1].Name) 52 | t.Log(neatjsons.S(res)) 53 | } 54 | { 55 | var one Example 56 | qx := columnName.Qx("=?", "abc").NOT() 57 | t.Log(qx.Qs()) 58 | t.Log(qx.Args()) 59 | require.NoError(t, db.Where(qx.Qs(), qx.Args()).First(&one).Error) 60 | require.NotEqual(t, "abc", one.Name) 61 | t.Log(neatjsons.S(one)) 62 | } 63 | }) 64 | } 65 | 66 | func TestColumnQx_AND_2(t *testing.T) { 67 | type Example struct { 68 | Name string `gorm:"primary_key;type:varchar(100);"` 69 | Type string `gorm:"column:type;"` 70 | } 71 | 72 | const ( 73 | columnName = ColumnName[string]("name") 74 | columnType = ColumnName[string]("type") 75 | ) 76 | 77 | tests.NewDBRun(t, func(db *gorm.DB) { 78 | require.NoError(t, db.AutoMigrate(&Example{})) 79 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 80 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 81 | 82 | { 83 | var one Example 84 | require.NoError(t, db.Where(columnName.Qx("=?", "abc").AND(columnType.Qx("=?", "xyz")).Qx2()).First(&one).Error) 85 | require.Equal(t, "abc", one.Name) 86 | t.Log(neatjsons.S(one)) 87 | } 88 | { 89 | var res []*Example 90 | require.NoError(t, db.Where(columnName.Qx("=?", "abc").OR(columnName.Qx("=?", "aaa")).Qx2()).Find(&res).Error) 91 | require.Contains(t, []string{"abc", "aaa"}, res[0].Name) 92 | require.Contains(t, []string{"abc", "aaa"}, res[1].Name) 93 | t.Log(neatjsons.S(res)) 94 | } 95 | { 96 | var one Example 97 | require.NoError(t, db.Where(columnName.Qx("=?", "abc").NOT().Qx1()).First(&one).Error) 98 | require.NotEqual(t, "abc", one.Name) 99 | t.Log(neatjsons.S(one)) 100 | } 101 | }) 102 | } 103 | 104 | func TestColumnQx_AND_3(t *testing.T) { 105 | type Example struct { 106 | Name string `gorm:"primary_key;type:varchar(100);"` 107 | Type string `gorm:"column:type;"` 108 | } 109 | 110 | const ( 111 | columnName = ColumnName[string]("name") 112 | columnType = ColumnName[string]("type") 113 | ) 114 | 115 | tests.NewDBRun(t, func(db *gorm.DB) { 116 | require.NoError(t, db.AutoMigrate(&Example{})) 117 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 118 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 119 | 120 | { 121 | var one Example 122 | require.NoError(t, db.Where(NewQx(columnName.Eq("abc")).AND(NewQx(columnType.Eq("xyz"))).Qx2()).First(&one).Error) 123 | require.Equal(t, "abc", one.Name) 124 | t.Log(neatjsons.S(one)) 125 | } 126 | { 127 | var res []*Example 128 | require.NoError(t, db.Where(NewQx(columnName.Eq("abc")).OR(NewQx(columnName.Eq("aaa"))).Qx2()).Find(&res).Error) 129 | require.Contains(t, []string{"abc", "aaa"}, res[0].Name) 130 | require.Contains(t, []string{"abc", "aaa"}, res[1].Name) 131 | t.Log(neatjsons.S(res)) 132 | } 133 | { 134 | var one Example 135 | require.NoError(t, db.Where(NewQx(columnName.Eq("abc")).NOT().Qx1()).First(&one).Error) 136 | require.NotEqual(t, "abc", one.Name) 137 | t.Log(neatjsons.S(one)) 138 | } 139 | }) 140 | } 141 | 142 | func TestColumnQx_AND_4(t *testing.T) { 143 | type Example struct { 144 | Name string `gorm:"primary_key;type:varchar(100);"` 145 | Type string `gorm:"column:type;"` 146 | } 147 | 148 | const ( 149 | columnName = ColumnName[string]("name") 150 | columnType = ColumnName[string]("type") 151 | ) 152 | 153 | tests.NewDBRun(t, func(db *gorm.DB) { 154 | require.NoError(t, db.AutoMigrate(&Example{})) 155 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 156 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 157 | 158 | { 159 | var one Example 160 | require.NoError(t, db.Where(Qx(columnName.Eq("abc")).AND(Qx(columnType.Eq("xyz"))).Qx2()).First(&one).Error) 161 | require.Equal(t, "abc", one.Name) 162 | t.Log(neatjsons.S(one)) 163 | } 164 | { 165 | var res []*Example 166 | require.NoError(t, db.Where(Qx(columnName.Eq("abc")).OR(Qx(columnName.Eq("aaa"))).Qx2()).Find(&res).Error) 167 | require.Contains(t, []string{"abc", "aaa"}, res[0].Name) 168 | require.Contains(t, []string{"abc", "aaa"}, res[1].Name) 169 | t.Log(neatjsons.S(res)) 170 | } 171 | { 172 | var one Example 173 | require.NoError(t, db.Where(Qx(columnName.Eq("abc")).NOT().Qx1()).First(&one).Error) 174 | require.NotEqual(t, "abc", one.Name) 175 | t.Log(neatjsons.S(one)) 176 | } 177 | }) 178 | } 179 | 180 | func TestColumnQx_AND_5(t *testing.T) { 181 | type Example struct { 182 | Name string `gorm:"primary_key;type:varchar(100);"` 183 | Type string `gorm:"column:type;"` 184 | } 185 | 186 | const ( 187 | columnName = ColumnName[string]("name") 188 | columnType = ColumnName[string]("type") 189 | ) 190 | 191 | tests.NewDBRun(t, func(db *gorm.DB) { 192 | require.NoError(t, db.AutoMigrate(&Example{})) 193 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 194 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 195 | 196 | { 197 | var one Example 198 | require.NoError(t, db.Where(Qx(columnName.BetweenAND("aba", "abd")). 199 | AND(Qx(columnType.IsNotNULL())).Qx2()). 200 | First(&one).Error) 201 | require.Equal(t, "abc", one.Name) 202 | t.Log(neatjsons.S(one)) 203 | } 204 | }) 205 | } 206 | -------------------------------------------------------------------------------- /internal/docs/CREATION_IDEAS.zh.md: -------------------------------------------------------------------------------- 1 | # 创作思路 2 | 3 | ## 问题 4 | 遇到的问题是,在频繁修改 models 的时候,gorm的 `db.Where("name=?", req.Name)` 的逻辑有可能会因为 `name` 被删除/改名/修改类型,而在运行时报错。 5 | 6 | 当然还有个特别小的问题是,单词`name`容易拼错,当然这个单词当然不会错,但是假如有很多其它字段呢,因此通常都是拷贝粘贴最靠谱,但这也会让开发变得不太顺滑。 7 | 8 | 当然主要的问题,还是在于项目随时可能重构,或者修改需求等,想要“确保models不常修改”,这其实是一种奢望。 9 | 10 | 特别是在项目创建初期,需求刚确定,就会很频繁的改动。 11 | 而在业务的后期,将会面临20个甚至更多的models在同一个项目里的情况,假设有三五个表都有 `name` 这个列,假设你需要删除某个模型的 `name`,就会非常麻烦,这样业务修改的灵活性将逐渐丧失,比如,因为不确定是否有用,因此模型/数据表/schema就只能增加字段,而不能删除字段。 12 | 面对代码里到处都有的 `Where("name=?", req.Name)` 再牛逼的程序员都会瑟瑟发抖/慎重对待的的吧。就算能集中精神解决这些问题,也是浪费精力的啊,我们人的精力都是有限的,只有让基础的事情简单化,这样才有可能抵达更深的深度,否则各种疥癣之疾都能把人折腾的头晕脑胀的,也就没法向更深的地方想啦。 13 | 14 | ## 方案 15 | 这个时候聪明的你想到了可以把跟某个模型相关的操作,单独写到一个文件里,比如创建 `repositories` 目录和 `repositories/example.go` 代码文件,这里面只操作 `Example` 模型。 16 | 这样做确实是能解决大部分问题,在业务代码 `service` 中涉及到读写 db 的时候,总是要调用 `repositories.FindExampleXxx(x,y,z)` 或者 `repositories.UpdateExampleXxx(x,y,z)` 的操作。 17 | 当然假如模型特别多的时候还可以按模型分类操作,比如 `repositories.ExampleRepo(db).FindXxx(x,y,z)` 或者 `exampleRepo.UpdateXxx(x,y,z)` 这样分类写操作。 18 | 但这样有个很大的弊端就是,读代码时总是要跳转的。 19 | 其次是,有些操作其实就是只需要在特定的场景下调用一次,完全没必要封装的,但按照上述的原则/约定,依然要封装函数。 20 | 还有就是,稍微复杂的查询其参数也不止两三个,面对`exampleRepo.UpdateXxx(x,y,z,u,v,w)` 这样的调用,会不会心里一惊,或者假如不小心写成`(x,y,z,w,v,u)`是不是也看不出来,毕竟函数内部逻辑用的形参和外部调用传的实参要对应位置,这本身就是个易错的事情。这就需要把参数名定的有意义些,就会让函数参数名特别长。 21 | 当然基于读代码要跳转的前提,在形参和实参间反复比对,查看传递路线也是有点费劲的。 22 | 23 | 因此这时候聪明的你想到我们可以用其它orm,比如 `go get entgo.io/ent` 能根据你的模型配置来生成代码,提供`func (ec *ExampleCreate) SetName(s string) *ExampleCreate` 和 `func (eu *ExampleUpdate) SetName(s string) *ExampleUpdate` 的操作,很明显,假如你需要改模型,改完生成代码后就能在静态检查阶段确保你的使用完全正确。 24 | 这个确实是个非常好用的包。 25 | 但问题是我不太喜欢用,没有特别的理由,估计是用 `gorm` 用的太久了不太习惯用别的,新东西总是要学习的,我学`go get entgo.io/ent`也只是学了个皮毛,而且我认为 `gorm` 是最强的。 26 | 27 | 因此我就想,我能不能基于 `gorm` 做个东西呢。 刚开始我也是用的生成代码的路线,比如利用配置得到这样的代码: 28 | ``` 29 | func (G *DemoGorm) Where名字(v string) *DemoGorm { return G.Where名字QS("= ?", v) } 30 | func (G *DemoGorm) Where日期(v string) *DemoGorm { return G.Where日期QS("= ?", v) } 31 | func (G *DemoGorm) Where数值(v float64) *DemoGorm { return G.Where数值QS("= ?", v) } 32 | ``` 33 | 但我很快就否了自己的这个方案,因为实在是太垃圾,就是随便定义个模型,就要生成数百行代码,而用的时候还是很蹩脚的,而且过多的无用代码也使得我的项目变得很臃肿,IDE也很卡顿,当然最可怕的是,自己封装的东西越用人就越傻,将来找工作面试啥的,只会用自己的,而不能熟练掌握 `gorm` 或者 `entgo.io/ent` 这些标准的或者经典的工具,就糟糕啦。 34 | 35 | ## 结果 36 | 因此我放弃了自己生成代码的方案,而我也不擅长使用 `entgo.io/ent` 这个工具,而且也不是所有的项目都能用 `entgo.io/ent` 的,比如当我新进一个项目,而项目本身没有使用 `entgo.io/ent` 的时候。 37 | 38 | 因此我还是得用 `gorm` 的,但是随着 golang 推出泛型以后事情似乎是有所转机,我突然觉得,我应该尽量少的生成代码,甚至是不生成代码,而是提供个三方包,只要 import 就能赋予 gorm 泛型的能力。 39 | 40 | ## 期望 41 | 假如我的模型是 42 | ``` 43 | type Example struct { 44 | Name string `gorm:"primary_key;type:varchar(100);"` 45 | Type string `gorm:"column:type;"` 46 | Rank int `gorm:"column:rank;"` 47 | } 48 | ``` 49 | 我不希望这样做 50 | ``` 51 | var res Example 52 | err := db.Where("name=?", req.Name).First(&res).Error 53 | ``` 54 | 我希望能这样做 55 | ``` 56 | var res Example 57 | var cls = res.Columns() //这是幻想出来的操作,实际没有这个东西,但我期望有这个东西,最好是能从golang语言底层就支持,让class自动蕴含cls的orm属性信息 58 | err := db.Where(cls.Name +"=?", req.Name).First(&res).Error //注意,这里的cls值的就是 Example 的元数据(也可以说是schema或者字段定义存储类型) 59 | ``` 60 | 当然更进一步的 61 | ``` 62 | var res Example 63 | var cls = res.Columns() 64 | err := db.Where(cls.Name.Eq(req.Name)).First(&res).Error //让我们彻底放飞自我吧。假如 cls.Name.Eq(req.Name) 就是返回两个值 ("name=?", req.Name),则它的返回值就,恰好能被 db.Where() 接收,这样 Eq 函数本身是很容易实现的。 65 | ``` 66 | 既然有这个期望 67 | 就试着实现它吧 68 | 69 | ## 结果 70 | 根据前面的期望,因此我需要以“列”为对象,让它具备泛型的效果,首先瞄准 `Eq` 这个函数去想象 71 | ``` 72 | type ColumnName[TYPE any] string 73 | 74 | func (s ColumnName[TYPE]) Eq(x TYPE) (string, TYPE) { 75 | return string(s) + "=?", x 76 | } 77 | ``` 78 | 假设定义个变量 79 | ``` 80 | var name = ColumnName[string]("name") //岂不是说就能调用 name.Eq("abc") 返回 ("name=?", "abc") 两个值啦 81 | ``` 82 | 而且因为泛型定义它是sting的,想使用 `name.Eq(1)` 就会在静态检查阶段/编译阶段报错啦 83 | 84 | 接下来其它的操作不也就顺理成章了嘛,"=" ">" "<" ">=" "<=" "!=" "IN" "BETWEEN...AND..." 这些操作简直是手到擒来啊。常用的 "AND" "OR" "NOT" "ORDER" 这些也都是有实现的 85 | ``` 86 | func (s ColumnName[TYPE]) Gt(x TYPE) (string, TYPE) { 87 | return string(s) + ">?", x 88 | } 89 | 90 | func (s ColumnName[TYPE]) Lt(x TYPE) (string, TYPE) { 91 | return string(s) + "100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 116 | ``` 117 | 结果相当完美。 118 | 119 | ## 优势 120 | 我认为这个工具它有诸多的优势: 121 | 1. 就像上面的自测代码那样,你只需要 `go get github.com/yyle88/gormcnm` 再定义 const/vars 就能享受到这个包带来的便利,只定义一两个列也是可以的。 122 | 2. 基本能够看到,这套基于`gorm`的东西,没有改变 `gorm` 的使用方法和逻辑,不仅让你的代码具备可读性。 123 | 3. 由于沿用 `gorm` 查询逻辑,它也不会影响你学习 `gorm` 这个非常牛逼的东西。这个我认为非常重要,就是工具不能让开发者变傻,假如让开发者变傻不擅长使用通用的东西,就不好啦,就有可能让开发者在面试时吃亏,我自己也是开发者我当然不希望有这样的负面作用。 124 | 4. 使用这个东西,还会让你更擅长使用 `gorm`。 125 | 5. 这个工具非常轻量级,就像上面的自测的代码,在代码里可以随时选择使用/不使用这个工具。 126 | 假如你没有定义 rank 字段的列常量,你就直接用原来的 Where("rank>?", 100) 就行。这个有啥优势呢,这个优势很大很大,它就能让我的工具不需要做得很完整,毕竟大家都是很忙的,有啥没实现的功能,就直接用原来的 `gorm` 解决就行,这样我也就没必要把全部东西都做出来啦(毕竟免费做东西造个轮子就行,没必要造车的)。 127 | 6. 由于可以随时用或者不用这个工具,把这个工具引入到现用使用 `gorm` 项目里的代价就很小,而且不会破坏任何东西。 128 | 129 | ## 潜力 130 | 使用这个工具你需要 `go get github.com/yyle88/gormcnm` 和 `import "github.com/yyle88/gormcnm"` 再定义列的常量: 131 | ``` 132 | const ( 133 | columnName = gormcnm.ColumnName[string]("name") 134 | columnType = gormcnm.ColumnName[string]("type") 135 | columnRank = gormcnm.ColumnName[int]("rank") 136 | ) 137 | ``` 138 | 但是很明显的,根据 models 的定义能通过分析知道有哪些列,也就是说这些字段定义都是【有可能】通过 `generate` 而手段自动生成的。【实际已经实现啦】 139 | 140 | 而且,很明显的,这几个常量没有 namespace 的概念。 141 | 假设在其它表里 `rank` 这个字段不再是 `int` 而是 `string` "S" "A" "B" "C" 呢,再定义常量,岂不是容易混淆啦。 142 | 143 | 因此你要这样做 144 | ``` 145 | type ExampleColumns struct { 146 | Name gormcnm.ColumnName[string] 147 | Type gormcnm.ColumnName[string] 148 | Rank gormcnm.ColumnName[int] 149 | } 150 | 151 | func (*Example) Columns() *ExampleColumns { 152 | return &ExampleColumns{ 153 | Name: "name", 154 | Type: "type", 155 | Rank: "rank", 156 | } 157 | } 158 | ``` 159 | 因此你要这样用 160 | ``` 161 | var res models.Example 162 | var cls = res.Columns() 163 | err := db.Where(cls.Name.Eq("abc")). 164 | Where(cls.Type.Eq("xyz")). 165 | Where(cls.Rank.Gt(100)). 166 | Where(cls.Rank.Lt(200)). 167 | First(&res).Error 168 | ``` 169 | 因此很明显的,我写的生成代码的工具自动生成出来的代码,就是这样的。 170 | ``` 171 | go get github.com/yyle88/gormcngen 172 | ``` 173 | 至于这个生成工具的用法就请移步到对应的项目里查看他的文档吧。[自动生成 gormcnm 模型字段的工具 gormcngen](https://github.com/yyle88/gormcngen) 174 | 175 | ## 其它 176 | 这个项目其实是致敬 `gorm` 的,但 `gormcnm` 或许有些不雅观,假如觉得不适也可以看这里 [gormrepo](https://github.com/yyle88/gormrepo) 177 | 我偶尔也会认为这个名字 `gormcls` 其实更好些,特别是以前用python某个orm的时候就是使用 `a.cls` 操作的。 178 | 但很显然这个项目解决的是“跟列名字段名有关的操作”,因此 `gorm column name` 缩写为 `gormcnm` 简直不能再合适啦,响亮而文雅,齐得隆东强。 179 | 180 | ## 完结 181 | 这个创作背景和使用方法介绍,完。 182 | 183 | Give me stars. Thank you!!! 184 | -------------------------------------------------------------------------------- /column_name_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm tests validate core column operations including math expressions and aggregates 2 | // Auto verifies ExprAdd, ExprSub, ExprMul, ExprDiv, ExprConcat, ExprReplace operations 3 | // Tests cover basic expressions, aggregation functions, and column method operations 4 | // 5 | // gormcnm 测试包验证核心列操作,包括数学表达式和聚合函数 6 | // 自动验证 ExprAdd、ExprSub、ExprMul、ExprDiv、ExprConcat、ExprReplace 操作 7 | // 测试涵盖基础表达式、聚合函数和列方法功能 8 | package gormcnm 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/gormcnm/internal/tests" 15 | "github.com/yyle88/neatjson/neatjsons" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | func TestColumnName_SafeCnm(t *testing.T) { 20 | type Example struct { 21 | Name string `gorm:"primary_key;type:varchar(100);"` 22 | Create string `gorm:"column:create"` 23 | } 24 | 25 | const columnCreate = ColumnName[string]("create") 26 | 27 | tests.NewDBRun(t, func(db *gorm.DB) { 28 | require.NoError(t, db.AutoMigrate(&Example{})) 29 | require.NoError(t, db.Save(&Example{ 30 | Name: "aaa", 31 | Create: "abc", 32 | }).Error) 33 | require.NoError(t, db.Save(&Example{ 34 | Name: "xxx", 35 | Create: "xyz", 36 | }).Error) 37 | require.NoError(t, db.Save(&Example{ 38 | Name: "uuu", 39 | Create: "uvw", 40 | }).Error) 41 | 42 | { 43 | var one Example 44 | require.NoError(t, db.Where(columnCreate.SafeCnm(`""`).Eq("abc")).First(&one).Error) 45 | require.Equal(t, "aaa", one.Name) 46 | t.Log(neatjsons.S(one)) 47 | } 48 | { 49 | var one Example 50 | require.NoError(t, db.Where(columnCreate.SafeCnm("`").Eq("xyz")).First(&one).Error) 51 | require.Equal(t, "xxx", one.Name) 52 | t.Log(neatjsons.S(one)) 53 | } 54 | { 55 | var one Example 56 | require.NoError(t, db.Where(columnCreate.SafeCnm("[]").Eq("uvw")).First(&one).Error) 57 | require.Equal(t, "uuu", one.Name) 58 | t.Log(neatjsons.S(one)) 59 | } 60 | { 61 | var one Example 62 | require.NoError(t, db.Where(columnCreate.SafeCnm("[-quote-]").Eq("uvw")).First(&one).Error) 63 | require.Equal(t, "uuu", one.Name) 64 | t.Log(neatjsons.S(one)) 65 | } 66 | }) 67 | } 68 | 69 | func TestColumnName_Count(t *testing.T) { 70 | type Example struct { 71 | Name string `gorm:"primary_key;type:varchar(100);"` 72 | Type string `gorm:"column:type;"` 73 | } 74 | 75 | const columnName = ColumnName[string]("name") 76 | 77 | tests.NewDBRun(t, func(db *gorm.DB) { 78 | require.NoError(t, db.AutoMigrate(&Example{})) 79 | require.NoError(t, db.Save(&Example{Name: "abc", Type: "xyz"}).Error) 80 | require.NoError(t, db.Save(&Example{Name: "aaa", Type: "xxx"}).Error) 81 | 82 | { 83 | var value int 84 | err := db.Model(&Example{}).Select(columnName.Count("cnt")).First(&value).Error 85 | require.NoError(t, err) 86 | require.Equal(t, 2, value) 87 | } 88 | { 89 | type resType struct { 90 | Cnt int64 91 | } 92 | var res resType 93 | err := db.Model(&Example{}).Select(columnName.CountDistinct("cnt")).First(&res).Error 94 | require.NoError(t, err) 95 | require.Equal(t, int64(2), res.Cnt) 96 | } 97 | }) 98 | } 99 | 100 | func TestColumnName_ExprOperations(t *testing.T) { 101 | type Example struct { 102 | ID uint `gorm:"primary_key"` 103 | Price float64 `gorm:"column:price"` 104 | Amount int `gorm:"column:amount"` 105 | Title string `gorm:"column:title"` 106 | } 107 | 108 | const ( 109 | columnPrice = ColumnName[float64]("price") 110 | columnAmount = ColumnName[int]("amount") 111 | ) 112 | 113 | tests.NewDBRun(t, func(db *gorm.DB) { 114 | require.NoError(t, db.AutoMigrate(&Example{})) 115 | require.NoError(t, db.Create(&Example{ 116 | ID: 1, 117 | Price: 100.0, 118 | Amount: 10, 119 | Title: "Product", 120 | }).Error) 121 | 122 | // Test ExprAdd and ExprMul with UpdateColumns 123 | { 124 | updateMap := map[string]interface{}{} 125 | key1, expr1 := columnPrice.KeAdd(10.0) // price = price + 10 126 | key2, expr2 := columnAmount.KeMul(2) // quantity = quantity * 2 127 | updateMap[key1] = expr1 128 | updateMap[key2] = expr2 129 | 130 | result := db.Model(&Example{}).Where("id = ?", 1).UpdateColumns(updateMap) 131 | require.NoError(t, result.Error) 132 | require.Equal(t, int64(1), result.RowsAffected) 133 | 134 | var updated Example 135 | require.NoError(t, db.First(&updated, 1).Error) 136 | require.Equal(t, 110.0, updated.Price) // 100 + 10 137 | require.Equal(t, 20, updated.Amount) // 10 * 2 138 | t.Log("updated result:", neatjsons.S(updated)) 139 | } 140 | 141 | // Test ExprSub and ExprDiv 142 | { 143 | updateMap := map[string]interface{}{} 144 | key1, expr1 := columnPrice.KeSub(10.0) // price = price - 10 145 | key2, expr2 := columnAmount.KeDiv(4) // quantity = quantity / 4 146 | updateMap[key1] = expr1 147 | updateMap[key2] = expr2 148 | 149 | result := db.Model(&Example{}).Where("id = ?", 1).UpdateColumns(updateMap) 150 | require.NoError(t, result.Error) 151 | require.Equal(t, int64(1), result.RowsAffected) 152 | 153 | var updated Example 154 | require.NoError(t, db.First(&updated, 1).Error) 155 | require.Equal(t, 100.0, updated.Price) // 110 - 10 156 | require.Equal(t, 5, updated.Amount) // 20 / 4 157 | t.Log("updated result:", neatjsons.S(updated)) 158 | } 159 | }) 160 | } 161 | 162 | func TestColumnName_StringExprOperations(t *testing.T) { 163 | type Example struct { 164 | ID uint `gorm:"primary_key"` 165 | Title string `gorm:"column:title"` 166 | Email string `gorm:"column:email"` 167 | } 168 | 169 | const ( 170 | columnTitle = ColumnName[string]("title") 171 | columnEmail = ColumnName[string]("email") 172 | ) 173 | 174 | tests.NewDBRun(t, func(db *gorm.DB) { 175 | require.NoError(t, db.AutoMigrate(&Example{})) 176 | require.NoError(t, db.Create(&Example{ 177 | ID: 1, 178 | Title: "Product", 179 | Email: "user@company.com", 180 | }).Error) 181 | 182 | // Test ExprConcat 183 | { 184 | updateMap := map[string]interface{}{} 185 | key1, expr1 := columnTitle.KeConcat(" [Hot]") // title = CONCAT(title, ' [Hot]') 186 | updateMap[key1] = expr1 187 | 188 | result := db.Model(&Example{}).Where("id = ?", 1).UpdateColumns(updateMap) 189 | require.NoError(t, result.Error) 190 | require.Equal(t, int64(1), result.RowsAffected) 191 | 192 | var updated Example 193 | require.NoError(t, db.First(&updated, 1).Error) 194 | require.Equal(t, "Product [Hot]", updated.Title) 195 | t.Log("updated result:", neatjsons.S(updated)) 196 | } 197 | 198 | // Test ExprReplace 199 | { 200 | updateMap := map[string]interface{}{} 201 | key1, expr1 := columnEmail.KeReplace("@company.com", "@newcompany.com") // email = REPLACE(email, '@company.com', '@newcompany.com') 202 | updateMap[key1] = expr1 203 | 204 | result := db.Model(&Example{}).Where("id = ?", 1).UpdateColumns(updateMap) 205 | require.NoError(t, result.Error) 206 | require.Equal(t, int64(1), result.RowsAffected) 207 | 208 | var updated Example 209 | require.NoError(t, db.First(&updated, 1).Error) 210 | require.Equal(t, "user@newcompany.com", updated.Email) 211 | t.Log("updated result:", neatjsons.S(updated)) 212 | } 213 | }) 214 | } 215 | -------------------------------------------------------------------------------- /gormcnmjson/json_test.go: -------------------------------------------------------------------------------- 1 | package gormcnmjson_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/yyle88/gormcnm" 9 | "github.com/yyle88/gormcnm/gormcnmjson" 10 | "github.com/yyle88/gormcnm/internal/tests" 11 | "github.com/yyle88/must" 12 | "gorm.io/datatypes" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type Product struct { 17 | Code string `gorm:"primarykey"` 18 | Name string `gorm:"type:varchar(100)"` 19 | Meta datatypes.JSON `gorm:"type:json"` 20 | } 21 | 22 | const ( 23 | columnCode = gormcnm.ColumnName[string]("code") 24 | columnName = gormcnm.ColumnName[string]("name") 25 | columnMeta = gormcnm.ColumnName[datatypes.JSON]("meta") 26 | ) 27 | 28 | func TestColumn_Get(t *testing.T) { 29 | tests.NewDBRun(t, func(db *gorm.DB) { 30 | must.Done(db.AutoMigrate(&Product{})) 31 | 32 | products := []Product{ 33 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"brand":"Apple","price":999}`))}, 34 | {Code: "P002", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"brand":"HuaWei","price":899}`))}, 35 | {Code: "P003", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"brand":"XiaoMi","price":799}`))}, 36 | } 37 | must.Done(db.Create(&products).Error) 38 | 39 | var results []Product 40 | require.NoError(t, db.Where(gormcnmjson.Raw(columnMeta).Get("brand").Eq("Apple")).Find(&results).Error) 41 | 42 | require.Len(t, results, 1) 43 | require.Equal(t, "iPhone", results[0].Name) 44 | }) 45 | } 46 | 47 | func TestColumn_GetInt(t *testing.T) { 48 | tests.NewDBRun(t, func(db *gorm.DB) { 49 | must.Done(db.AutoMigrate(&Product{})) 50 | 51 | products := []Product{ 52 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"brand":"Apple","price":999}`))}, 53 | {Code: "P002", Name: "MacBook", Meta: datatypes.JSON([]byte(`{"brand":"Apple","price":1999}`))}, 54 | {Code: "P003", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"brand":"HuaWei","price":699}`))}, 55 | {Code: "P004", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"brand":"XiaoMi","price":899}`))}, 56 | } 57 | must.Done(db.Create(&products).Error) 58 | 59 | var results []Product 60 | require.NoError(t, db.Where(gormcnmjson.Raw(columnMeta).GetInt("price").Gt(1000)).Find(&results).Error) 61 | 62 | require.Len(t, results, 1) 63 | require.Equal(t, "MacBook", results[0].Name) 64 | }) 65 | } 66 | 67 | func TestColumn_Length(t *testing.T) { 68 | tests.NewDBRun(t, func(db *gorm.DB) { 69 | must.Done(db.AutoMigrate(&Product{})) 70 | 71 | products := []Product{ 72 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"tags":["phone","5G","iOS"]}`))}, 73 | {Code: "P002", Name: "MacBook", Meta: datatypes.JSON([]byte(`{"tags":["laptop","macOS"]}`))}, 74 | {Code: "P003", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"tags":["phone","5G"]}`))}, 75 | {Code: "P004", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"tags":["phone"]}`))}, 76 | } 77 | must.Done(db.Create(&products).Error) 78 | 79 | var results []Product 80 | require.NoError(t, db.Where(gormcnmjson.Raw(columnMeta).Length("tags").Gt(2)).Find(&results).Error) 81 | 82 | require.Len(t, results, 1) 83 | require.Equal(t, "iPhone", results[0].Name) 84 | }) 85 | } 86 | 87 | func TestColumn_Extract(t *testing.T) { 88 | tests.NewDBRun(t, func(db *gorm.DB) { 89 | must.Done(db.AutoMigrate(&Product{})) 90 | 91 | products := []Product{ 92 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"specs":{"storage":"128GB","chip":"A17"}}`))}, 93 | {Code: "P002", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"specs":{"storage":"256GB","chip":"Kirin"}}`))}, 94 | {Code: "P003", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"specs":{"storage":"512GB","chip":"Snapdragon"}}`))}, 95 | } 96 | must.Done(db.Create(&products).Error) 97 | 98 | var result struct { 99 | Name string 100 | Storage string 101 | } 102 | require.NoError(t, db.Table("products"). 103 | Select(string(columnName)+", "+string(columnMeta)+" ->> '$.specs.storage' as storage"). 104 | Where(string(columnName)+" = ?", "iPhone"). 105 | First(&result).Error) 106 | 107 | require.Equal(t, "iPhone", result.Name) 108 | require.Equal(t, "128GB", result.Storage) 109 | }) 110 | } 111 | 112 | func TestColumn_Type(t *testing.T) { 113 | tests.NewDBRun(t, func(db *gorm.DB) { 114 | must.Done(db.AutoMigrate(&Product{})) 115 | 116 | products := []Product{ 117 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"key":"value"}`))}, 118 | {Code: "P002", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"key":"value"}`))}, 119 | {Code: "P003", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"key":"value"}`))}, 120 | } 121 | must.Done(db.Create(&products).Error) 122 | 123 | var result struct { 124 | JSONType string 125 | } 126 | require.NoError(t, db.Table("products"). 127 | Select("JSON_TYPE("+string(columnMeta)+") as json_type"). 128 | Limit(1). 129 | Scan(&result).Error) 130 | 131 | require.Equal(t, "object", result.JSONType) 132 | }) 133 | } 134 | 135 | func TestColumn_Valid(t *testing.T) { 136 | tests.NewDBRun(t, func(db *gorm.DB) { 137 | must.Done(db.AutoMigrate(&Product{})) 138 | 139 | products := []Product{ 140 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"valid":true}`))}, 141 | {Code: "P002", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"valid":true}`))}, 142 | {Code: "P003", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"valid":true}`))}, 143 | } 144 | must.Done(db.Create(&products).Error) 145 | 146 | var result struct { 147 | IsValid int 148 | } 149 | require.NoError(t, db.Table("products"). 150 | Select("JSON_VALID("+string(columnMeta)+") as is_valid"). 151 | Limit(1). 152 | Scan(&result).Error) 153 | 154 | require.Equal(t, 1, result.IsValid) 155 | }) 156 | } 157 | 158 | func TestColumn_Update(t *testing.T) { 159 | tests.NewDBRun(t, func(db *gorm.DB) { 160 | must.Done(db.AutoMigrate(&Product{})) 161 | 162 | products := []Product{ 163 | {Code: "P001", Name: "iPhone", Meta: datatypes.JSON([]byte(`{"brand":"Apple","price":999}`))}, 164 | {Code: "P002", Name: "Mate60", Meta: datatypes.JSON([]byte(`{"brand":"HuaWei","price":899}`))}, 165 | {Code: "P003", Name: "Mi14", Meta: datatypes.JSON([]byte(`{"brand":"XiaoMi","price":799}`))}, 166 | } 167 | must.Done(db.Create(&products).Error) 168 | 169 | require.NoError(t, db.Model(&Product{}). 170 | Where(columnCode.Eq("P001")). 171 | Update(string(columnMeta), gorm.Expr(gormcnmjson.Raw(columnMeta).Set("price", 1099).Name())).Error) 172 | 173 | var updated Product 174 | require.NoError(t, db.Where(columnCode.Eq("P001")).First(&updated).Error) 175 | 176 | var meta map[string]interface{} 177 | require.NoError(t, json.Unmarshal(updated.Meta, &meta)) 178 | require.Equal(t, "1099", meta["price"]) 179 | }) 180 | } 181 | 182 | func TestColumn_New(t *testing.T) { 183 | type ProductWithString struct { 184 | Code string `gorm:"primarykey"` 185 | Name string `gorm:"type:varchar(100)"` 186 | Meta string `gorm:"type:json"` 187 | } 188 | 189 | const stringMeta = gormcnm.ColumnName[string]("meta") 190 | 191 | tests.NewDBRun(t, func(db *gorm.DB) { 192 | must.Done(db.AutoMigrate(&ProductWithString{})) 193 | 194 | products := []ProductWithString{ 195 | {Code: "P001", Name: "iPhone", Meta: `{"brand":"Apple","price":999}`}, 196 | {Code: "P002", Name: "Mate60", Meta: `{"brand":"HuaWei","price":899}`}, 197 | {Code: "P003", Name: "Mi14", Meta: `{"brand":"XiaoMi","price":799}`}, 198 | } 199 | must.Done(db.Create(&products).Error) 200 | 201 | var results []ProductWithString 202 | require.NoError(t, db.Where(gormcnmjson.New(stringMeta).Get("brand").Eq("Apple")).Find(&results).Error) 203 | 204 | require.Len(t, results, 1) 205 | require.Equal(t, "iPhone", results[0].Name) 206 | }) 207 | } 208 | -------------------------------------------------------------------------------- /gormcnmstub/stub_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnmstub_test validates stub implementation with integration tests 2 | // Auto verifies stub wrapping functions with actual GORM database operations 3 | // Tests examine condition logic, select statements, and column operations 4 | // 5 | // gormcnmstub_test 包通过集成测试验证存根实现 6 | // 自动使用真实 GORM 数据库操作验证存根包装函数 7 | // 测试涵盖查询条件、select 语句和列操作 8 | package gormcnmstub_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/done" 15 | "github.com/yyle88/gormcnm" 16 | "github.com/yyle88/gormcnm/gormcnmstub" 17 | "github.com/yyle88/gormcnm/internal/tests" 18 | "github.com/yyle88/neatjson/neatjsons" 19 | "gorm.io/gorm" 20 | ) 21 | 22 | func TestExample000(t *testing.T) { 23 | type Example struct { 24 | Name string `gorm:"primary_key;type:varchar(100);"` 25 | Type string `gorm:"column:type;"` 26 | Rank int `gorm:"column:rank;"` 27 | } 28 | 29 | const ( 30 | columnName = gormcnm.ColumnName[string]("name") 31 | columnType = gormcnm.ColumnName[string]("type") 32 | columnRank = gormcnm.ColumnName[int]("rank") 33 | ) 34 | 35 | tests.NewDBRun(t, func(db *gorm.DB) { 36 | done.Done(db.AutoMigrate(&Example{})) 37 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 38 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 39 | 40 | { 41 | result := db.Model(&Example{}).Where( 42 | gormcnmstub.NewQx( 43 | columnName.Eq("abc"), 44 | ).AND1( 45 | columnType.Eq("xyz"), 46 | ).AND1( 47 | columnRank.Eq(123), 48 | ).Qx3(), 49 | ).UpdateColumns(columnRank.Kw(200).Kw(columnType.Kv("www")).Map()) 50 | require.NoError(t, result.Error) 51 | require.Equal(t, int64(1), result.RowsAffected) 52 | } 53 | { 54 | stmt := db.Model(&Example{}) 55 | stmt = gormcnmstub.Where(stmt, gormcnmstub.Qx(columnName.Eq("aaa")). 56 | AND( 57 | gormcnmstub.Qx(columnType.Eq("xxx")), 58 | gormcnmstub.Qx(columnRank.Eq(456)), 59 | ). 60 | OR1(columnName.Eq("abc")), 61 | ) 62 | var results []*Example 63 | require.NoError(t, stmt.Find(&results).Error) 64 | t.Log(neatjsons.S(results)) 65 | } 66 | { 67 | stmt := db.Model(&Example{}) 68 | stmt = gormcnmstub.Where(stmt, gormcnmstub.Qx(columnName.Eq("aaa")). 69 | AND( 70 | gormcnmstub.Qx(columnType.Eq("xxx")), 71 | gormcnmstub.Qx(columnRank.Eq(456)), 72 | ), 73 | ) 74 | result := gormcnmstub.UpdateColumns(stmt, gormcnmstub.NewKw().Kw(columnRank.Kv(100)).Kw(columnType.Kv("uvw"))) 75 | require.NoError(t, result.Error) 76 | require.Equal(t, int64(1), result.RowsAffected) 77 | } 78 | { 79 | stmt := db.Model(&Example{}) 80 | stmt = gormcnmstub.Where(stmt, gormcnmstub.Qx(columnName.Eq("abc")).OR(gormcnmstub.Qx(columnName.Eq("aaa")))) 81 | stmt = gormcnmstub.OrderByColumns(stmt, columnRank.Ob("asc")) 82 | 83 | var examples []*Example 84 | result := stmt.Find(&examples) 85 | require.NoError(t, result.Error) 86 | require.Equal(t, 2, len(examples)) 87 | require.Equal(t, 100, examples[0].Rank) 88 | require.Equal(t, 200, examples[1].Rank) 89 | t.Log(neatjsons.S(examples)) 90 | } 91 | }) 92 | } 93 | 94 | func TestExample001(t *testing.T) { 95 | type Example struct { 96 | Name string `gorm:"primary_key;type:varchar(100);"` 97 | Type string `gorm:"column:type;"` 98 | Rank int `gorm:"column:rank;"` 99 | } 100 | 101 | const columnName = gormcnm.ColumnName[string]("name") 102 | 103 | tests.NewDBRun(t, func(db *gorm.DB) { 104 | done.Done(db.AutoMigrate(&Example{})) 105 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 106 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 107 | 108 | type resType struct { 109 | Who string 110 | Cnt int64 111 | } 112 | 113 | { 114 | var results []resType 115 | require.NoError(t, db.Model(&Example{}). 116 | Group(columnName.Name()). 117 | Select(gormcnmstub.MergeStmts( 118 | columnName.AsAlias("who"), 119 | gormcnmstub.CountStmt("cnt"), 120 | )). 121 | Find(&results).Error) 122 | require.Equal(t, 2, len(results)) 123 | for _, one := range results { 124 | t.Log(one.Who, one.Cnt) 125 | require.NotEmpty(t, one.Who) 126 | require.Positive(t, one.Cnt) 127 | } 128 | } 129 | { 130 | var results []resType 131 | require.NoError(t, db.Model(&Example{}). 132 | Group(columnName.Name()). 133 | Select(gormcnmstub.MergeStmts( 134 | columnName.AsAlias("who"), 135 | columnName.Count("cnt"), 136 | )). 137 | Find(&results).Error) 138 | require.Equal(t, 2, len(results)) 139 | for _, one := range results { 140 | t.Log(one.Who, one.Cnt) 141 | require.NotEmpty(t, one.Who) 142 | require.Positive(t, one.Cnt) 143 | } 144 | } 145 | { 146 | var results []resType 147 | require.NoError(t, db.Model(&Example{}). 148 | Group(columnName.Name()). 149 | Select(gormcnmstub.MergeStmts( 150 | columnName.AsAlias("who"), 151 | columnName.CountDistinct("cnt"), 152 | )). 153 | Find(&results).Error) 154 | require.Equal(t, 2, len(results)) 155 | for _, one := range results { 156 | t.Log(one.Who, one.Cnt) 157 | require.NotEmpty(t, one.Who) 158 | require.Positive(t, one.Cnt) 159 | } 160 | } 161 | }) 162 | } 163 | 164 | func TestExample002(t *testing.T) { 165 | type Example struct { 166 | Name string `gorm:"primary_key;type:varchar(100);"` 167 | Type string `gorm:"column:type;"` 168 | Rank int `gorm:"column:rank;"` 169 | } 170 | 171 | tests.NewDBRun(t, func(db *gorm.DB) { 172 | done.Done(db.AutoMigrate(&Example{})) 173 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 174 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 175 | 176 | type resType struct { 177 | Cnt int64 178 | } 179 | 180 | var res resType 181 | require.NoError(t, db.Model(&Example{}).Select(gormcnmstub.CountStmt("cnt")).Find(&res).Error) 182 | require.Equal(t, int64(2), res.Cnt) 183 | }) 184 | } 185 | 186 | func TestExample003(t *testing.T) { 187 | type Example struct { 188 | Name string `gorm:"primary_key;type:varchar(100);"` 189 | Type string `gorm:"column:type;"` 190 | Rank int `gorm:"column:rank;"` 191 | } 192 | 193 | const ( 194 | columnName = gormcnm.ColumnName[string]("name") 195 | columnType = gormcnm.ColumnName[string]("type") 196 | ) 197 | 198 | tests.NewDBRun(t, func(db *gorm.DB) { 199 | done.Done(db.AutoMigrate(&Example{})) 200 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 201 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 202 | 203 | type resType struct { 204 | Cnt int64 205 | } 206 | 207 | { 208 | var res resType 209 | stmt := gormcnmstub.CountCaseWhenStmt(columnName.Name()+"="+"'aaa'", "cnt") 210 | t.Log(stmt) 211 | require.NoError(t, db.Model(&Example{}).Select(stmt).Find(&res).Error) 212 | require.Equal(t, int64(1), res.Cnt) 213 | } 214 | 215 | { 216 | var res resType 217 | stmt := gormcnmstub.CountCaseWhenStmt(columnName.Name()+"="+"'aaa'"+" AND "+columnType.Name()+"="+"'xxx'", "cnt") 218 | t.Log(stmt) 219 | require.NoError(t, db.Model(&Example{}).Select(stmt).Find(&res).Error) 220 | require.Equal(t, int64(1), res.Cnt) 221 | } 222 | 223 | { 224 | var results []*Example 225 | var qx = gormcnmstub.NewQx(columnName.Eq("aaa")).AND1(columnType.Eq("xxx")) 226 | t.Log(qx.Qs()) 227 | require.NoError(t, db.Model(&Example{}).Where(qx.Qx2()).Find(&results).Error) 228 | t.Log(len(results)) 229 | } 230 | 231 | { 232 | var res resType 233 | var qx = gormcnmstub.NewQx(columnName.Eq("aaa")).AND1(columnType.Eq("xxx")) 234 | t.Log(qx.Qs()) 235 | var sx = gormcnmstub.CountCaseWhenQxSx(qx, "cnt") 236 | t.Log(sx.Qs()) 237 | require.NoError(t, db.Model(&Example{}).Select(sx.Qx2()).Find(&res).Error) 238 | require.Equal(t, int64(1), res.Cnt) 239 | } 240 | 241 | { 242 | var res resType 243 | var qx = gormcnmstub.NewQx(columnName.Eq("aaa")).AND1(columnType.Eq("xxx")) 244 | t.Log(qx.Qs()) 245 | var sx = gormcnmstub.CountCaseWhenQxSx(qx, "cnt") 246 | t.Log(sx.Qs()) 247 | db = db.Model(&Example{}) 248 | db = gormcnmstub.Select(db, sx) 249 | require.NoError(t, db.Find(&res).Error) 250 | require.Equal(t, int64(1), res.Cnt) 251 | } 252 | }) 253 | } 254 | -------------------------------------------------------------------------------- /op_common_test.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm_test provides comprehensive integration tests with gormcnm features 2 | // Auto validates column operations, where generation, and GORM integration patterns 3 | // Tests demonstrate real-world usage scenarios with an SQLite memory-based database 4 | // 5 | // gormcnm_test 包为 gormcnm 功能提供全面的集成测试 6 | // 自动验证列操作、查询生成和 GORM 集成模式 7 | // 测试展示了使用 SQLite 内存数据库的实际使用场景 8 | package gormcnm_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | "github.com/yyle88/done" 15 | "github.com/yyle88/gormcnm" 16 | "github.com/yyle88/gormcnm/internal/tests" 17 | "github.com/yyle88/neatjson/neatjsons" 18 | "gorm.io/gorm" 19 | ) 20 | 21 | func TestExample000(t *testing.T) { 22 | type Example struct { 23 | Name string `gorm:"primary_key;type:varchar(100);"` 24 | Type string `gorm:"column:type;"` 25 | Rank int `gorm:"column:rank;"` 26 | } 27 | 28 | const ( 29 | columnName = gormcnm.ColumnName[string]("name") 30 | columnType = gormcnm.ColumnName[string]("type") 31 | columnRank = gormcnm.ColumnName[int]("rank") 32 | ) 33 | 34 | operation := &gormcnm.ColumnOperationClass{} 35 | 36 | tests.NewDBRun(t, func(db *gorm.DB) { 37 | done.Done(db.AutoMigrate(&Example{})) 38 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 39 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 40 | 41 | { 42 | result := db.Model(&Example{}).Where( 43 | operation.NewQx( 44 | columnName.Eq("abc"), 45 | ).AND1( 46 | columnType.Eq("xyz"), 47 | ).AND1( 48 | columnRank.Eq(123), 49 | ).Qx3(), 50 | ).UpdateColumns(columnRank.Kw(200).Kw(columnType.Kv("www")).Map()) 51 | require.NoError(t, result.Error) 52 | require.Equal(t, int64(1), result.RowsAffected) 53 | } 54 | { 55 | stmt := db.Model(&Example{}) 56 | stmt = operation.Where(stmt, operation.Qx(columnName.Eq("aaa")). 57 | AND( 58 | operation.Qx(columnType.Eq("xxx")), 59 | operation.Qx(columnRank.Eq(456)), 60 | ). 61 | OR1(columnName.Eq("abc")), 62 | ) 63 | var results []*Example 64 | require.NoError(t, stmt.Find(&results).Error) 65 | t.Log(neatjsons.S(results)) 66 | } 67 | { 68 | stmt := db.Model(&Example{}) 69 | stmt = operation.Where(stmt, operation.Qx(columnName.Eq("aaa")). 70 | AND( 71 | operation.Qx(columnType.Eq("xxx")), 72 | operation.Qx(columnRank.Eq(456)), 73 | ), 74 | ) 75 | result := operation.UpdateColumns(stmt, operation.NewKw().Kw(columnRank.Kv(100)).Kw(columnType.Kv("uvw"))) 76 | require.NoError(t, result.Error) 77 | require.Equal(t, int64(1), result.RowsAffected) 78 | } 79 | { 80 | stmt := db.Model(&Example{}) 81 | stmt = operation.Where(stmt, operation.Qx(columnName.Eq("abc")).OR(operation.Qx(columnName.Eq("aaa")))) 82 | stmt = operation.OrderByColumns(stmt, columnRank.Ob("asc")) 83 | 84 | var examples []*Example 85 | result := stmt.Find(&examples) 86 | require.NoError(t, result.Error) 87 | require.Equal(t, 2, len(examples)) 88 | require.Equal(t, 100, examples[0].Rank) 89 | require.Equal(t, 200, examples[1].Rank) 90 | t.Log(neatjsons.S(examples)) 91 | } 92 | }) 93 | } 94 | 95 | func TestExample001(t *testing.T) { 96 | type Example struct { 97 | Name string `gorm:"primary_key;type:varchar(100);"` 98 | Type string `gorm:"column:type;"` 99 | Rank int `gorm:"column:rank;"` 100 | } 101 | 102 | const columnName = gormcnm.ColumnName[string]("name") 103 | 104 | operation := &gormcnm.ColumnOperationClass{} 105 | 106 | tests.NewDBRun(t, func(db *gorm.DB) { 107 | done.Done(db.AutoMigrate(&Example{})) 108 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 109 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 110 | 111 | type resType struct { 112 | Who string 113 | Cnt int64 114 | } 115 | 116 | { 117 | var results []resType 118 | require.NoError(t, db.Model(&Example{}). 119 | Group(columnName.Name()). 120 | Select(operation.MergeStmts( 121 | columnName.AsAlias("who"), 122 | operation.CountStmt("cnt"), 123 | )). 124 | Find(&results).Error) 125 | require.Equal(t, 2, len(results)) 126 | for _, one := range results { 127 | t.Log(one.Who, one.Cnt) 128 | require.NotEmpty(t, one.Who) 129 | require.Positive(t, one.Cnt) 130 | } 131 | } 132 | { 133 | var results []resType 134 | require.NoError(t, db.Model(&Example{}). 135 | Group(columnName.Name()). 136 | Select(operation.MergeStmts( 137 | columnName.AsAlias("who"), 138 | columnName.Count("cnt"), 139 | )). 140 | Find(&results).Error) 141 | require.Equal(t, 2, len(results)) 142 | for _, one := range results { 143 | t.Log(one.Who, one.Cnt) 144 | require.NotEmpty(t, one.Who) 145 | require.Positive(t, one.Cnt) 146 | } 147 | } 148 | { 149 | var results []resType 150 | require.NoError(t, db.Model(&Example{}). 151 | Group(columnName.Name()). 152 | Select(operation.MergeStmts( 153 | columnName.AsAlias("who"), 154 | columnName.CountDistinct("cnt"), 155 | )). 156 | Find(&results).Error) 157 | require.Equal(t, 2, len(results)) 158 | for _, one := range results { 159 | t.Log(one.Who, one.Cnt) 160 | require.NotEmpty(t, one.Who) 161 | require.Positive(t, one.Cnt) 162 | } 163 | } 164 | }) 165 | } 166 | 167 | func TestExample002(t *testing.T) { 168 | type Example struct { 169 | Name string `gorm:"primary_key;type:varchar(100);"` 170 | Type string `gorm:"column:type;"` 171 | Rank int `gorm:"column:rank;"` 172 | } 173 | 174 | operation := &gormcnm.ColumnOperationClass{} 175 | 176 | tests.NewDBRun(t, func(db *gorm.DB) { 177 | done.Done(db.AutoMigrate(&Example{})) 178 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 179 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 180 | 181 | type resType struct { 182 | Cnt int64 183 | } 184 | 185 | var res resType 186 | require.NoError(t, db.Model(&Example{}).Select(operation.CountStmt("cnt")).Find(&res).Error) 187 | require.Equal(t, int64(2), res.Cnt) 188 | }) 189 | } 190 | 191 | func TestExample003(t *testing.T) { 192 | type Example struct { 193 | Name string `gorm:"primary_key;type:varchar(100);"` 194 | Type string `gorm:"column:type;"` 195 | Rank int `gorm:"column:rank;"` 196 | } 197 | 198 | const ( 199 | columnName = gormcnm.ColumnName[string]("name") 200 | columnType = gormcnm.ColumnName[string]("type") 201 | ) 202 | 203 | operation := &gormcnm.ColumnOperationClass{} 204 | 205 | tests.NewDBRun(t, func(db *gorm.DB) { 206 | done.Done(db.AutoMigrate(&Example{})) 207 | done.Done(db.Save(&Example{Name: "abc", Type: "xyz", Rank: 123}).Error) 208 | done.Done(db.Save(&Example{Name: "aaa", Type: "xxx", Rank: 456}).Error) 209 | 210 | type resType struct { 211 | Cnt int64 212 | } 213 | 214 | { 215 | var res resType 216 | stmt := operation.CountCaseWhenStmt(columnName.Name()+"="+"'aaa'", "cnt") 217 | t.Log(stmt) 218 | require.NoError(t, db.Model(&Example{}).Select(stmt).Find(&res).Error) 219 | require.Equal(t, int64(1), res.Cnt) 220 | } 221 | 222 | { 223 | var res resType 224 | stmt := operation.CountCaseWhenStmt(columnName.Name()+"="+"'aaa'"+" AND "+columnType.Name()+"="+"'xxx'", "cnt") 225 | t.Log(stmt) 226 | require.NoError(t, db.Model(&Example{}).Select(stmt).Find(&res).Error) 227 | require.Equal(t, int64(1), res.Cnt) 228 | } 229 | 230 | { 231 | var results []*Example 232 | var qx = operation.NewQx(columnName.Eq("aaa")).AND1(columnType.Eq("xxx")) 233 | t.Log(qx.Qs()) 234 | require.NoError(t, db.Model(&Example{}).Where(qx.Qx2()).Find(&results).Error) 235 | t.Log(len(results)) 236 | } 237 | 238 | { 239 | var res resType 240 | var qx = operation.NewQx(columnName.Eq("aaa")).AND1(columnType.Eq("xxx")) 241 | t.Log(qx.Qs()) 242 | var sx = operation.CountCaseWhenQxSx(qx, "cnt") 243 | t.Log(sx.Qs()) 244 | require.NoError(t, db.Model(&Example{}).Select(sx.Qx2()).Find(&res).Error) 245 | require.Equal(t, int64(1), res.Cnt) 246 | } 247 | 248 | { 249 | var res resType 250 | var qx = operation.NewQx(columnName.Eq("aaa")).AND1(columnType.Eq("xxx")) 251 | t.Log(qx.Qs()) 252 | var sx = operation.CountCaseWhenQxSx(qx, "cnt") 253 | t.Log(sx.Qs()) 254 | db = db.Model(&Example{}) 255 | db = operation.Select(db, sx) 256 | require.NoError(t, db.Find(&res).Error) 257 | require.Equal(t, int64(1), res.Cnt) 258 | } 259 | }) 260 | } 261 | 262 | func TestColumnOperationClass_MergeSlices(t *testing.T) { 263 | operation := &gormcnm.ColumnOperationClass{} 264 | 265 | slice1 := []string{"a", "b", "c"} 266 | slice2 := []string{"d", "e", "f"} 267 | slice3 := []string{"g", "h"} 268 | 269 | results := operation.MergeSlices(slice1, slice2, slice3) 270 | require.Equal(t, "a, b, c, d, e, f, g, h", results) 271 | } 272 | -------------------------------------------------------------------------------- /statement_args.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides statement and arguments tuple handling in GORM queries operations 2 | // Auto manages SQL statements with argument binding and type conversion 3 | // Supports driver.Valuer interface implementation, enabling custom types in GORM WHERE clauses 4 | // 5 | // gormcnm 提供语句和参数元组处理,用于 GORM 查询操作 6 | // 自动管理 SQL 语句及参数绑定和类型转换 7 | // 支持 driver.Valuer 接口实现,用于 GORM WHERE 子句中的自定义类型 8 | package gormcnm 9 | 10 | import ( 11 | "database/sql/driver" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/yyle88/must" 15 | ) 16 | 17 | // 当你在调用时报这个错时,说明你 where 条件的第一个参数不是字符串类型,而是直接使用的该项目中自定义的类型,而 gorm 不是能自动识别它们,因此我主动增加 panic 以提醒您出现错误 18 | // 因为 gorm 中的 db.Where 的定义是这样的 19 | // func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) 20 | // 在这个项目里,你需要传的是,查询字符串 stmt 和其参数列表 args... 而不是其它类型 21 | // 因此你需要传的是 db.Where(qx.Qx1()) db.Where(qx.Qx2()) db.Where(qx.Qx3()) 而不是 db.Where(qx) 22 | // 这块非常抱歉,但目前尚无特别好的解决方案,因为 Where 需要的是变长的参数列表,而 golang 不支持变长个数的返回值,返回个数组是没用的 23 | var valueIsNotCallable = errors.New("column.value() function is not callable") 24 | 25 | // statementArgumentsTuple represents a tuple of SQL statement and its arguments 26 | // Core building block in GORM queries construction with suitable argument handling 27 | // Addresses Go language limitation where variadic return values are not supported 28 | // 29 | // 核心设计说明: 30 | // - GORM 的 Where 和 Select 方法需要: func (db *DB) Where(query interface{}, args ...interface{}) 31 | // - 当遇到 AND/OR 时需要合并语句和参数列表 32 | // - 由于 Go 不支持变长返回值,只能设计 Qx1(), Qx2(), Qx3()等方法 33 | // - 包含 panic 机制防止直接传递给 GORM 导致的错误使用 34 | // 35 | // statementArgumentsTuple 表示 SQL 语句及其参数的元组 36 | // GORM 查询构建的核心构建块,提供适当的参数处理 37 | // 解决 Go 语言不支持变长返回值的限制 38 | type statementArgumentsTuple struct { 39 | stmt string // SQL statement string // SQL 语句字符串 40 | args []interface{} // Prepared statement arguments // 预处理语句参数 41 | } 42 | 43 | // newStatementArgumentsTuple creates a new statementArgumentsTuple instance with the provided statement and arguments. 44 | // newStatementArgumentsTuple 使用提供的语句和参数创建一个新的 statementArgumentsTuple 实例。 45 | func newStatementArgumentsTuple(stmt string, args []interface{}) *statementArgumentsTuple { 46 | return &statementArgumentsTuple{ 47 | stmt: stmt, 48 | args: args, 49 | } 50 | } 51 | 52 | // safeCombineArguments merges the arguments from the current instance with the provided instances and returns the combined arguments list. 53 | // safeCombineArguments 将当前实例的参数与提供的实例的参数合并,并返回合并后的参数列表。 54 | func (qx *statementArgumentsTuple) safeCombineArguments(cs []*statementArgumentsTuple) []interface{} { 55 | var args []interface{} 56 | args = append(args, qx.args...) 57 | for _, c := range cs { 58 | args = append(args, c.args...) 59 | } 60 | return args 61 | } 62 | 63 | // Value panics to prevent direct use of this structure in GORM, as it is not callable in this context. 64 | // Value 会触发 panic,以防止在 GORM 中直接使用此结构,因为在此上下文中无法调用该函数。 65 | func (qx *statementArgumentsTuple) Value() (driver.Value, error) { 66 | panic(valueIsNotCallable) //当报这个错时,需要修改调用侧代码,请看这个错误码的注释 67 | } 68 | 69 | // Qs return the statement string of the current statementArgumentsTuple instance. 70 | // Qs 返回当前 statementArgumentsTuple 实例的语句字符串。 71 | func (qx *statementArgumentsTuple) Qs() string { 72 | return qx.stmt 73 | } 74 | 75 | // Args return the arguments list of the current statementArgumentsTuple instance. 76 | // Args 返回当前 statementArgumentsTuple 实例的参数列表。 77 | func (qx *statementArgumentsTuple) Args() []interface{} { 78 | return qx.args 79 | } 80 | 81 | // Qx0 returns the statement string when there are no arguments in the statementArgumentsTuple instance. 82 | // Qx0 在 statementArgumentsTuple 实例没有参数时返回语句字符串。 83 | func (qx *statementArgumentsTuple) Qx0() string { 84 | must.Len(qx.args, 0) 85 | return qx.Qs() 86 | } 87 | 88 | // Qx1 returns the statement string and the first argument in the statementArgumentsTuple instance. 89 | // Qx1 返回 statementArgumentsTuple 实例的语句字符串和第一个参数。 90 | func (qx *statementArgumentsTuple) Qx1() (string, interface{}) { 91 | must.Len(qx.args, 1) 92 | return qx.Qs(), qx.args[0] 93 | } 94 | 95 | // Qx2 returns the statement string and the first two arguments in the statementArgumentsTuple instance. 96 | // Qx2 返回 statementArgumentsTuple 实例的语句字符串和前两个参数。 97 | func (qx *statementArgumentsTuple) Qx2() (string, interface{}, interface{}) { 98 | must.Len(qx.args, 2) 99 | return qx.Qs(), qx.args[0], qx.args[1] 100 | } 101 | 102 | // Qx3 returns the statement string and three arguments in the statementArgumentsTuple instance. 103 | // Qx3 返回 statementArgumentsTuple 实例的语句字符串和三个参数。 104 | func (qx *statementArgumentsTuple) Qx3() (string, interface{}, interface{}, interface{}) { 105 | must.Len(qx.args, 3) 106 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2] 107 | } 108 | 109 | // Qx4 returns the statement string and four arguments in the statementArgumentsTuple instance. 110 | // Qx4 返回 statementArgumentsTuple 实例的语句字符串和四个参数。 111 | func (qx *statementArgumentsTuple) Qx4() (string, interface{}, interface{}, interface{}, interface{}) { 112 | must.Len(qx.args, 4) 113 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3] 114 | } 115 | 116 | // Qx5 returns the statement string and five arguments in the statementArgumentsTuple instance. 117 | // Qx5 返回 statementArgumentsTuple 实例的语句字符串和五个参数。 118 | func (qx *statementArgumentsTuple) Qx5() (string, interface{}, interface{}, interface{}, interface{}, interface{}) { 119 | must.Len(qx.args, 5) 120 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4] 121 | } 122 | 123 | // Qx6 returns the statement string and six arguments in the statementArgumentsTuple instance. 124 | // Qx6 返回 statementArgumentsTuple 实例的语句字符串和六个参数。 125 | func (qx *statementArgumentsTuple) Qx6() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 126 | must.Len(qx.args, 6) 127 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5] 128 | } 129 | 130 | // Qx7 returns the statement string and seven arguments in the statementArgumentsTuple instance. 131 | // Qx7 返回 statementArgumentsTuple 实例的语句字符串和七个参数。 132 | func (qx *statementArgumentsTuple) Qx7() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 133 | must.Len(qx.args, 7) 134 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5], qx.args[6] 135 | } 136 | 137 | // Qx8 returns the statement string and eight arguments in the statementArgumentsTuple instance. 138 | // Qx8 返回 statementArgumentsTuple 实例的语句字符串和八个参数。 139 | func (qx *statementArgumentsTuple) Qx8() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 140 | must.Len(qx.args, 8) 141 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5], qx.args[6], qx.args[7] 142 | } 143 | 144 | // Qx9 returns the statement string and nine arguments in the statementArgumentsTuple instance. 145 | // Qx9 返回 statementArgumentsTuple 实例的语句字符串和九个参数。 146 | func (qx *statementArgumentsTuple) Qx9() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 147 | must.Len(qx.args, 9) 148 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5], qx.args[6], qx.args[7], qx.args[8] 149 | } 150 | 151 | // Qx10 returns the statement string and ten arguments in the statementArgumentsTuple instance. 152 | // Qx10 返回 statementArgumentsTuple 实例的语句字符串和十个参数。 153 | func (qx *statementArgumentsTuple) Qx10() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 154 | must.Len(qx.args, 10) 155 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5], qx.args[6], qx.args[7], qx.args[8], qx.args[9] 156 | } 157 | 158 | // Qx11 returns the statement string and eleven arguments in the statementArgumentsTuple instance. 159 | // Qx11 返回 statementArgumentsTuple 实例的语句字符串和十一个参数。 160 | func (qx *statementArgumentsTuple) Qx11() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 161 | must.Len(qx.args, 11) 162 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5], qx.args[6], qx.args[7], qx.args[8], qx.args[9], qx.args[10] 163 | } 164 | 165 | // Qx12 returns the statement string and twelve arguments in the statementArgumentsTuple instance. 166 | // Qx12 返回 statementArgumentsTuple 实例的语句字符串和十二个参数。 167 | func (qx *statementArgumentsTuple) Qx12() (string, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}, interface{}) { 168 | must.Len(qx.args, 12) 169 | return qx.Qs(), qx.args[0], qx.args[1], qx.args[2], qx.args[3], qx.args[4], qx.args[5], qx.args[6], qx.args[7], qx.args[8], qx.args[9], qx.args[10], qx.args[11] 170 | } 171 | -------------------------------------------------------------------------------- /op_common.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides common column operation patterns and utilities functions 2 | // Auto exposes ColumnOperationClass with queries conditions, select statements, and JOIN operations 3 | // Supports building reusable operation patterns with scope management and variable tracking 4 | // 5 | // gormcnm 包提供常用列操作模式和实用函数 6 | // 自动暴露 ColumnOperationClass,包含查询条件、select 语句和 JOIN 操作 7 | // 支持构建可重用的操作模式,具有作用域管理和变量追踪 8 | package gormcnm 9 | 10 | import ( 11 | "strings" 12 | 13 | "github.com/yyle88/gormcnm/internal/utils" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | // ColumnOperationClass provides a set of common methods to handle column operations in database queries. 18 | // ColumnOperationClass 提供一组常用的数据库列操作工具函数。 19 | type ColumnOperationClass struct{} 20 | 21 | // OK always returns true and provides a simple condition. 22 | // Its main purpose is to provide a compact scope in which variables exist just within 23 | // the condition block. Scope limitation keeps code clean and makes the logic simple to understand. 24 | // 25 | // OK 始终返回 true,提供一个简单的条件。 26 | // 主要目的是提供一个紧凑的作用域,变量仅在条件块内有效。 27 | // 作用域限制使代码整洁,逻辑易于理解。 28 | func (common *ColumnOperationClass) OK() bool { 29 | return true 30 | } 31 | 32 | // CreateCondition creates a new QxConjunction with the provided statement and arguments. 33 | // CreateCondition 根据提供的语句和参数创建一个新的 QxConjunction。 34 | func (common *ColumnOperationClass) CreateCondition(stmt string, args ...interface{}) *QxConjunction { 35 | return NewQxConjunction(stmt, args...) 36 | } 37 | 38 | // NewQx creates a new QxConjunction with the provided statement and arguments. 39 | // NewQx 根据提供的语句和参数创建一个新的 QxConjunction。 40 | func (common *ColumnOperationClass) NewQx(stmt string, args ...interface{}) *QxConjunction { 41 | return NewQxConjunction(stmt, args...) 42 | } 43 | 44 | // Qx returns a new QxConjunction with the provided statement and arguments. 45 | // Qx 返回一个新的 QxConjunction,使用提供的语句和参数。 46 | func (common *ColumnOperationClass) Qx(stmt string, args ...interface{}) *QxConjunction { 47 | return NewQxConjunction(stmt, args...) 48 | } 49 | 50 | // CreateSelect creates a new SelectStatement with the provided statement and arguments. 51 | // CreateSelect 根据提供的语句和参数创建一个新的 SelectStatement。 52 | func (common *ColumnOperationClass) CreateSelect(stmt string, args ...interface{}) *SelectStatement { 53 | return NewSelectStatement(stmt, args...) 54 | } 55 | 56 | // NewSx creates a new SelectStatement with the provided statement and arguments. 57 | // NewSx 根据提供的语句和参数创建一个新的 SelectStatement。 58 | func (common *ColumnOperationClass) NewSx(stmt string, args ...interface{}) *SelectStatement { 59 | return NewSelectStatement(stmt, args...) 60 | } 61 | 62 | // Sx returns a new SelectStatement with the provided statement and arguments. 63 | // Sx 返回一个新的 SelectStatement,使用提供的语句和参数。 64 | func (common *ColumnOperationClass) Sx(stmt string, args ...interface{}) *SelectStatement { 65 | return NewSelectStatement(stmt, args...) 66 | } 67 | 68 | // NewColumnValueMap creates a new ColumnValueMap using the NewKw function. 69 | // NewColumnValueMap 使用 NewKw 函数创建一个新的 ColumnValueMap。 70 | func (common *ColumnOperationClass) NewColumnValueMap() ColumnValueMap { 71 | return NewKw() 72 | } 73 | 74 | // NewKw creates a new ColumnValueMap using the Kw function. 75 | // NewKw 使用 Kw 函数创建一个新的 ColumnValueMap。 76 | func (common *ColumnOperationClass) NewKw() ColumnValueMap { 77 | return NewKw() 78 | } 79 | 80 | // CreateColumnValueMap creates a ColumnValueMap using the provided column name and value. 81 | // CreateColumnValueMap 根据提供的列名和值创建一个 ColumnValueMap。 82 | func (common *ColumnOperationClass) CreateColumnValueMap(columnName string, value interface{}) ColumnValueMap { 83 | return Kw(columnName, value) 84 | } 85 | 86 | // Kw creates a ColumnValueMap using the provided column name and value. 87 | // Kw 根据提供的列名和值创建一个 ColumnValueMap。 88 | func (common *ColumnOperationClass) Kw(columnName string, value interface{}) ColumnValueMap { 89 | return Kw(columnName, value) 90 | } 91 | 92 | // Where applies the provided QxConjunctions to the given gorm.DB statement. 93 | // Where 将提供的 QxConjunction 应用到给定的 gorm.DB 语句。 94 | func (common *ColumnOperationClass) Where(db *gorm.DB, qxs ...*QxConjunction) *gorm.DB { 95 | for _, qx := range qxs { 96 | db = db.Where(qx.Qs(), qx.args...) 97 | } 98 | return db 99 | } 100 | 101 | // OrderByColumns applies the provided OrderByBottle objects to the given gorm.DB statement. 102 | // OrderByColumns 将提供的 OrderByBottle 对象应用到给定的 gorm.DB 语句。 103 | func (common *ColumnOperationClass) OrderByColumns(db *gorm.DB, obs ...OrderByBottle) *gorm.DB { 104 | for _, ob := range obs { 105 | db = db.Order(ob.Ox()) 106 | } 107 | return db 108 | } 109 | 110 | // UpdateColumns updates the columns of the given gorm.DB statement with the provided ColumnValueMaps. 111 | // UpdateColumns 使用提供的 ColumnValueMap 更新给定的 gorm.DB 语句的列。 112 | func (common *ColumnOperationClass) UpdateColumns(db *gorm.DB, kws ...ColumnValueMap) *gorm.DB { 113 | mp := map[string]interface{}{} 114 | for _, kw := range kws { 115 | for k, v := range kw.AsMap() { 116 | mp[k] = v 117 | } 118 | } 119 | return db.UpdateColumns(mp) 120 | } 121 | 122 | // CombineColumnNames combines the names of the provided ColumnNameInterfaces into a single string. 123 | // CombineColumnNames 将提供的 ColumnNameInterface 的名称组合成一个字符串。 124 | func (common *ColumnOperationClass) CombineColumnNames(a ...utils.ColumnNameInterface) string { 125 | var names = make([]string, 0, len(a)) 126 | for _, x := range a { 127 | names = append(names, x.Name()) 128 | } 129 | return strings.Join(names, ", ") 130 | } 131 | 132 | // MergeNames combines the names of the provided ColumnNameInterfaces into a single string. 133 | // MergeNames 将提供的 ColumnNameInterface 的名称组合成一个字符串。 134 | func (common *ColumnOperationClass) MergeNames(a ...utils.ColumnNameInterface) string { 135 | return common.CombineColumnNames(a...) 136 | } 137 | 138 | // CombineNamesSlices combines multiple string slices into a single comma-separated string 139 | // CombineNamesSlices 将多个字符串切片组合成一个逗号分隔的字符串 140 | func (common *ColumnOperationClass) CombineNamesSlices(a ...[]string) string { 141 | var names []string 142 | for _, elems := range a { 143 | names = append(names, elems...) 144 | } 145 | return strings.Join(names, ", ") 146 | } 147 | 148 | // MergeSlices combines multiple string slices into a single comma-separated string 149 | // MergeSlices 将多个字符串切片组合成一个逗号分隔的字符串 150 | func (common *ColumnOperationClass) MergeSlices(a ...[]string) string { 151 | return common.CombineNamesSlices(a...) 152 | } 153 | 154 | // CombineStatements combines the provided SQL statements into a single string. 155 | // CombineStatements 将提供的 SQL 语句组合成一个字符串。 156 | func (common *ColumnOperationClass) CombineStatements(a ...string) string { 157 | return strings.Join(a, ", ") 158 | } 159 | 160 | // MergeStmts combines the provided SQL statements into a single string. 161 | // MergeStmts 将提供的 SQL 语句组合成一个字符串。 162 | func (common *ColumnOperationClass) MergeStmts(a ...string) string { 163 | return strings.Join(a, ", ") 164 | } 165 | 166 | // CountStmt returns a SQL statement that counts the records, applying the alias. 167 | // CountStmt 返回一个计算记录数量的 SQL 语句,并应用别名。 168 | func (common *ColumnOperationClass) CountStmt(alias string) string { 169 | return utils.ApplyAliasToColumn("COUNT(*)", alias) 170 | } 171 | 172 | // CountCaseWhenStmt returns a SQL statement that counts the records with a CASE WHEN condition, applying the alias. 173 | // CountCaseWhenStmt 返回一个计算记录数量的 SQL 语句,带有 CASE WHEN 条件,并应用别名。 174 | func (common *ColumnOperationClass) CountCaseWhenStmt(condition string, alias string) string { 175 | return utils.ApplyAliasToColumn("COUNT(CASE WHEN ("+condition+") THEN 1 END)", alias) 176 | } 177 | 178 | // CountCaseWhenQxSx returns a SelectStatement with a COUNT CASE WHEN condition applied, using the provided QxConjunction and alias. 179 | // CountCaseWhenQxSx 返回一个带有 COUNT CASE WHEN 条件的 SelectStatement,使用提供的 QxConjunction 和别名。 180 | func (common *ColumnOperationClass) CountCaseWhenQxSx(qx *QxConjunction, alias string) *SelectStatement { 181 | return NewSelectStatement( 182 | utils.ApplyAliasToColumn("COUNT(CASE WHEN ("+qx.Qs()+") THEN 1 END)", alias), 183 | qx.Args()..., 184 | ) 185 | } 186 | 187 | // CombineSelectStatements combines multiple SelectStatements into a single SelectStatement. 188 | // CombineSelectStatements 将多个 SelectStatement 组合成一个 SelectStatement。 189 | func (common *ColumnOperationClass) CombineSelectStatements(cs ...SelectStatement) *SelectStatement { 190 | var qsVs []string 191 | var args []any 192 | for _, c := range cs { 193 | qsVs = append(qsVs, c.Qs()) 194 | args = append(args, c.Args()...) 195 | } 196 | var stmt = strings.Join(qsVs, ", ") 197 | return NewSelectStatement(stmt, args...) 198 | } 199 | 200 | // CombineSxs combines multiple SelectStatements into a single SelectStatement. 201 | // CombineSxs 将多个 SelectStatement 组合成一个 SelectStatement。 202 | func (common *ColumnOperationClass) CombineSxs(cs ...SelectStatement) *SelectStatement { 203 | return common.CombineSelectStatements(cs...) 204 | } 205 | 206 | // Select applies the provided SelectStatements to the given gorm.DB statement. 207 | // Select 将提供的 SelectStatement 应用到给定的 gorm.DB 语句。 208 | func (common *ColumnOperationClass) Select(db *gorm.DB, qxs ...*SelectStatement) *gorm.DB { 209 | for _, qx := range qxs { 210 | db = db.Select(qx.Qs(), qx.args...) 211 | } 212 | return db 213 | } 214 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= 7 | github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 8 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 9 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 10 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 11 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 12 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 13 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 16 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 17 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 18 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= 19 | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 20 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 21 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 22 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 23 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 24 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 25 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 26 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 27 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 28 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 29 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 30 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 31 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 32 | github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= 33 | github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 34 | github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= 35 | github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= 36 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 37 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 38 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 40 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 42 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 43 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 44 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 45 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 46 | github.com/yyle88/done v1.0.28 h1:ZlC5ENTHAR0CQm19t1WhpbtKsKNPwsrXRtDewFsq4HA= 47 | github.com/yyle88/done v1.0.28/go.mod h1:dc0SzvQkX4NLEIz2shgYvETprQ6c0VZb+DCDtIi9n2Q= 48 | github.com/yyle88/erero v1.0.24 h1:yroawlW4IohY4bK4SonMBNI2tlZftPjtfhYYBtBfCxw= 49 | github.com/yyle88/erero v1.0.24/go.mod h1:KoTJmpyuKtQddt1QEQidwmewilCWwEH8rn5NsIAZ8XE= 50 | github.com/yyle88/formatgo v1.0.28 h1:xetQgoIpSnUkgL3EkPx3AP1Tl0uS/1d03NhmcFrWcqo= 51 | github.com/yyle88/formatgo v1.0.28/go.mod h1:3IwR7CXcv1UcZm6tLpaG/tCB4tXf5UktXS810zaPj7c= 52 | github.com/yyle88/must v0.0.28 h1:p6iO/tD3wuHfaiILQixI7IoZ501384wdAfgFnFlL1t0= 53 | github.com/yyle88/must v0.0.28/go.mod h1:3KE9bwEdCCodFDmyXQOMLnjKBI+TD0uW+tf1ZH0dxlo= 54 | github.com/yyle88/mutexmap v1.0.15 h1:vqwtvomfzddcuBNg8hofWnILRFK2STJhWU7AufuNS50= 55 | github.com/yyle88/mutexmap v1.0.15/go.mod h1:NqwsKlK+NkL18i4BepeyCgtenXuw4N5UUnEX9XBfPA8= 56 | github.com/yyle88/neatjson v0.0.13 h1:+1Ihb43IZLkYAd+lapnvUJN20bTjSAkoTE/2gssBew8= 57 | github.com/yyle88/neatjson v0.0.13/go.mod h1:BOgA69f27Bd/yj2xnIWldratF3WwIrMKrBgo9eIZGb0= 58 | github.com/yyle88/printgo v1.0.6 h1:b53uyCdlijObvuyHVECiiEWQIDfuOpuHVrkNQIdR5bU= 59 | github.com/yyle88/printgo v1.0.6/go.mod h1:14qsuuTovdfgHtle0e4ln6zci47Xtkdx9CCkUo3vxoc= 60 | github.com/yyle88/rese v0.0.12 h1:3cbPm5XmqPiRK2yj+nAUl50ci+9t8pEYOAVp+cKOX8w= 61 | github.com/yyle88/rese v0.0.12/go.mod h1:FGfU5brwe1PcyRobQh40/9gse51QVfJOLmBV/0DXSfA= 62 | github.com/yyle88/runpath v1.0.25 h1:WfeVd/7pLxpCvMhrLptKYNaplApJldveY9O7un67d3U= 63 | github.com/yyle88/runpath v1.0.25/go.mod h1:Iv9y6waZR95gUMMsK96WNAbF3wknfoPUd7xuQf/+CNY= 64 | github.com/yyle88/sure v0.0.42 h1:dsA7p/46AR9vanEPWYLJrlXEmwb+E/DePtImKzlKaBc= 65 | github.com/yyle88/sure v0.0.42/go.mod h1:G1YnfMptgQkkM+rOXoiAnFYt78z0fZMV3NTwHXfaCJU= 66 | github.com/yyle88/syntaxgo v0.0.53 h1:3W4S5ncRdq3hUp3Qjw4GqB+mAxypJCycMo/mMJP+1vc= 67 | github.com/yyle88/syntaxgo v0.0.53/go.mod h1:68EidTlDxVi/iaCJeg0menpA4v/xbq+ITxa1aKdmjLo= 68 | github.com/yyle88/tern v0.0.10 h1:2MvDoVyfIeEzskMpLE6B07dtRpSqI6LaeUD/mGuLW7o= 69 | github.com/yyle88/tern v0.0.10/go.mod h1:OHHE2G1gYaX4q0uu3sG9JAK9dBjHboxGcTXbRPVoGeQ= 70 | github.com/yyle88/zaplog v0.0.27 h1:Bd/XWeAeRDEsFdtHphEqPK+W3M9WNd/dzf5x6YXeSkY= 71 | github.com/yyle88/zaplog v0.0.27/go.mod h1:0BOxIR1lFh4vdiCyR5zuj4DmTFK36FbpjOWAdjMwSDU= 72 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 73 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 74 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 75 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 76 | go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 77 | go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 78 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 79 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 80 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= 81 | golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= 82 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 83 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 84 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 85 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 86 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 87 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 88 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 89 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 90 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 91 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 92 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 93 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 94 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= 96 | gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= 97 | gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= 98 | gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= 99 | gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= 100 | gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= 101 | gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= 102 | gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= 103 | gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= 104 | gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= 105 | gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= 106 | gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= 107 | -------------------------------------------------------------------------------- /column_name.go: -------------------------------------------------------------------------------- 1 | // Package gormcnm provides advanced column operations including math expressions and aggregate functions 2 | // Auto creates GORM expressions with ExprAdd, ExprSub, ExprMul, ExprDiv and aggregate operations 3 | // Supports building complex SQL operations with type-safe column expressions and GORM integration 4 | // 5 | // gormcnm 包提供高级列操作,包括数学表达式和聚合函数 6 | // 自动创建 GORM 表达式,包含 ExprAdd、ExprSub、ExprMul、ExprDiv 和聚合操作 7 | // 支持使用类型安全的列表达式构建复杂 SQL 操作,具有 GORM 集成 8 | package gormcnm 9 | 10 | import ( 11 | "github.com/yyle88/gormcnm/internal/utils" 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/clause" 14 | ) 15 | 16 | // SafeCnm returns a safe column name, enclosing it in backticks. 17 | // SafeCnm 返回一个安全的列名,将其用反引号括起来。 18 | // If the column name conflicts with a SQL keyword (e.g., "create"), enclosing it in backticks ensures correct execution. 19 | // 如果列名与 SQL 关键字(例如 "create")冲突,使用反引号将其括起来,确保正确执行。 20 | // Usage example: db.Select("`type`").Find(&one) demonstrates the standard pattern. 21 | // 使用示例:db.Select("`type`").Find(&one) 展示了标准使用模式。 22 | func (columnName ColumnName[TYPE]) SafeCnm(quote string) ColumnName[TYPE] { 23 | switch len(quote) { 24 | case 0: // If no quote is provided, we just add spaces around the column name. 25 | // 如果没有提供引号,我们将列名周围加上空格作为默认处理。 26 | return ColumnName[TYPE](" " + string(columnName) + " ") 27 | case 1: // When the quote has one byte like "`", `"` 28 | // 如果引号是一个单字符(例如 "`" 或 `"`) 29 | return ColumnName[TYPE](quote + string(columnName) + quote) 30 | case 2: // If the quote is two characters like `""`, "``", "[]" 31 | // 如果引号是两个字符(例如 `""`、 "``"、 "[]") 32 | return ColumnName[TYPE](quote[:1] + string(columnName) + quote[1:]) 33 | default: // Not recommended but not panic, use default quote 34 | // 不推荐这样做,但也不抛异常,给个默认的结果 35 | return ColumnName[TYPE](`"` + string(columnName) + `"`) 36 | } 37 | } 38 | 39 | // ExprAdd creates a GORM expression to add a value to the column. 40 | // ExprAdd: 创建一个 GORM 表达式,将一个值加到列中。 41 | func (columnName ColumnName[TYPE]) ExprAdd(v TYPE) clause.Expr { 42 | return gorm.Expr(string(columnName)+" + ?", v) 43 | } 44 | 45 | // ExprSub creates a GORM expression to subtract a value from the column. 46 | // ExprSub: 创建一个 GORM 表达式,从列中减去一个值。 47 | func (columnName ColumnName[TYPE]) ExprSub(v TYPE) clause.Expr { 48 | return gorm.Expr(string(columnName)+" - ?", v) 49 | } 50 | 51 | // ExprMul creates a GORM expression when doing multiplication with the column and a value. 52 | // ExprMul: 创建一个 GORM 表达式,将列乘以一个值。 53 | func (columnName ColumnName[TYPE]) ExprMul(v TYPE) clause.Expr { 54 | return gorm.Expr(string(columnName)+" * ?", v) 55 | } 56 | 57 | // ExprDiv creates a GORM expression to divide the column by a value. 58 | // ExprDiv: 创建一个 GORM 表达式,将列除以一个值。 59 | func (columnName ColumnName[TYPE]) ExprDiv(v TYPE) clause.Expr { 60 | return gorm.Expr(string(columnName)+" / ?", v) 61 | } 62 | 63 | // ExprConcat creates a GORM expression to concatenate a string to the column. 64 | // ExprConcat: 创建一个 GORM 表达式,将字符串连接到列。 65 | func (columnName ColumnName[TYPE]) ExprConcat(v TYPE) clause.Expr { 66 | return gorm.Expr("CONCAT("+string(columnName)+", ?)", v) 67 | } 68 | 69 | // ExprReplace creates a GORM expression to replace text in the column. 70 | // ExprReplace: 创建一个 GORM 表达式,替换列中的文本。 71 | func (columnName ColumnName[TYPE]) ExprReplace(oldValue, newValue TYPE) clause.Expr { 72 | return gorm.Expr("REPLACE("+string(columnName)+", ?, ?)", oldValue, newValue) 73 | } 74 | 75 | // Ob creates an ordering clause with the specified direction (ASC or DESC) when using this column. 76 | // Ob: 创建一个带有指定方向(ASC 或 DESC)的 ORDER BY 子句。 77 | func (columnName ColumnName[TYPE]) Ob(direction string) OrderByBottle { 78 | return OrderByBottle(string(columnName) + " " + direction) 79 | } 80 | 81 | // OrderByBottle creates an ordering clause with the given direction (ASC or DESC) when using this column. 82 | // OrderByBottle: 创建一个带有给定方向(ASC 或 DESC)的 ORDER BY 子句。 83 | func (columnName ColumnName[TYPE]) OrderByBottle(direction string) OrderByBottle { 84 | return OrderByBottle(string(columnName) + " " + direction) 85 | } 86 | 87 | // Qc creates a condition when using the provided op (e.g., '=', '>', etc.) with this column. 88 | // Qc: 使用提供的运算符(例如 '=', '>', 等)为列创建条件。 89 | func (columnName ColumnName[TYPE]) Qc(op string) QsConjunction { 90 | return QsConjunction(string(columnName) + " " + op) 91 | } 92 | 93 | // ColumnCondition creates a condition when using the provided op (e.g., '=', '>', etc.) with this column. 94 | // ColumnCondition: 使用提供的运算符(例如 '=', '>', 等)为列创建条件。 95 | func (columnName ColumnName[TYPE]) ColumnCondition(op string) QsConjunction { 96 | return QsConjunction(string(columnName) + " " + op) 97 | } 98 | 99 | // Qx creates a condition with an op and a value when using this column, designed to build complex queries. 100 | // Qx: 创建带有运算符和值的条件,用于构建复杂的查询。 101 | func (columnName ColumnName[TYPE]) Qx(op string, x TYPE) *QxConjunction { 102 | stmt := string(columnName.Qc(op)) 103 | args := []interface{}{x} 104 | return NewQxConjunction(stmt, args...) 105 | } 106 | 107 | // ColumnConditionWithValue creates a condition with an op and a value when using this column, designed to build complex queries. 108 | // ColumnConditionWithValue: 创建带有运算符和值的列条件,用于构建复杂的查询。 109 | func (columnName ColumnName[TYPE]) ColumnConditionWithValue(op string, x TYPE) *QxConjunction { 110 | stmt := string(columnName.ColumnCondition(op)) 111 | args := []interface{}{x} 112 | return NewQxConjunction(stmt, args...) 113 | } 114 | 115 | // Kw creates a map with a single column-value mapping. 116 | // Kw: 创建一个包含单个键值对的 map。 117 | func (columnName ColumnName[TYPE]) Kw(x TYPE) ColumnValueMap { 118 | return ColumnValueMap{string(columnName): x} 119 | } 120 | 121 | // CreateColumnValueMap creates a map with a single column-value mapping. 122 | // CreateColumnValueMap: 创建一个包含单个键值对的 map。 123 | func (columnName ColumnName[TYPE]) CreateColumnValueMap(x TYPE) ColumnValueMap { 124 | return ColumnValueMap{string(columnName): x} 125 | } 126 | 127 | // Kv returns a column-value mapping, works with GORM's Update function. 128 | // Kv: 返回键值对,适用于 GORM 的 Update 函数。 129 | func (columnName ColumnName[TYPE]) Kv(x TYPE) (string, TYPE) { 130 | return string(columnName), x 131 | } 132 | 133 | // ColumnKeyAndValue returns a column-value mapping, works with GORM's Update function. 134 | // ColumnKeyAndValue: 返回键值对,适用于 GORM 的 Update 函数。 135 | func (columnName ColumnName[TYPE]) ColumnKeyAndValue(x TYPE) (string, TYPE) { 136 | return string(columnName), x 137 | } 138 | 139 | // KeExp extends Kv by returning a column-expression mapping, works with GORM's Update function with expressions. 140 | // KeExp: 扩展 Kv,返回键表达式对,适用于 GORM 的 Update 函数,支持表达式。 141 | func (columnName ColumnName[TYPE]) KeExp(x clause.Expr) (string, clause.Expr) { 142 | return string(columnName), x 143 | } 144 | 145 | // KeAdd is used in updates where a value is added to the field (e.g., incrementing a value). 146 | // Returns (columnName, gorm.Expr) tuple to use in UpdateColumns operations. 147 | // 148 | // With Gorm: 149 | // 150 | // db.UpdateColumns(map[string]interface{}{"price": gorm.Expr("price + ?", 10)}) 151 | // 152 | // With KeAdd: 153 | // 154 | // db.UpdateColumns(map[string]interface{}{cls.Price.KeAdd(10)}) 155 | // 156 | // Both generate: "UPDATE ... SET price = price + 10" 157 | // 158 | // KeAdd: 用于更新字段时,将一个值加到字段上(例如递增一个值)。 159 | // 返回 (列名, gorm.Expr) 元组,用于 UpdateColumns 操作。 160 | // 161 | // 传统写法: 162 | // 163 | // db.UpdateColumns(map[string]interface{}{"price": gorm.Expr("price + ?", 10)}) 164 | // 165 | // 使用 KeAdd: 166 | // 167 | // db.UpdateColumns(map[string]interface{}{cls.Price.KeAdd(10)}) 168 | // 169 | // 都生成:"UPDATE ... SET price = price + 10" 170 | func (columnName ColumnName[TYPE]) KeAdd(x TYPE) (string, clause.Expr) { 171 | return columnName.KeExp(columnName.ExprAdd(x)) 172 | } 173 | 174 | // KeSub is used in updates where a value is subtracted from the field (e.g., decrementing a value). 175 | // Returns (columnName, gorm.Expr) tuple to use in UpdateColumns operations. 176 | // 177 | // With Gorm: 178 | // 179 | // db.UpdateColumns(map[string]interface{}{"stock": gorm.Expr("stock - ?", 1)}) 180 | // 181 | // With KeSub: 182 | // 183 | // db.UpdateColumns(map[string]interface{}{cls.Stock.KeSub(1)}) 184 | // 185 | // Both generate: "UPDATE ... SET stock = stock - 1" 186 | // 187 | // KeSub: 用于更新字段时,将一个值从字段中减去(例如递减一个值)。 188 | // 返回 (列名, gorm.Expr) 元组,用于 UpdateColumns 操作。 189 | // 190 | // 传统写法: 191 | // 192 | // db.UpdateColumns(map[string]interface{}{"stock": gorm.Expr("stock - ?", 1)}) 193 | // 194 | // 使用 KeSub: 195 | // 196 | // db.UpdateColumns(map[string]interface{}{cls.Stock.KeSub(1)}) 197 | // 198 | // 都生成:"UPDATE ... SET stock = stock - 1" 199 | func (columnName ColumnName[TYPE]) KeSub(x TYPE) (string, clause.Expr) { 200 | return columnName.KeExp(columnName.ExprSub(x)) 201 | } 202 | 203 | // KeMul is used in updates where a field is multiplied by a value (e.g., scaling a value). 204 | // KeMul: 用于更新字段时,将字段乘以一个值(例如缩放一个值)。 205 | func (columnName ColumnName[TYPE]) KeMul(x TYPE) (string, clause.Expr) { 206 | return columnName.KeExp(columnName.ExprMul(x)) 207 | } 208 | 209 | // KeDiv is used in updates where a field is divided by a value (e.g., splitting a value). 210 | // KeDiv: 用于更新字段时,将字段除以一个值(例如分割一个值)。 211 | func (columnName ColumnName[TYPE]) KeDiv(x TYPE) (string, clause.Expr) { 212 | return columnName.KeExp(columnName.ExprDiv(x)) 213 | } 214 | 215 | // KeConcat is used in updates where a string is concatenated to the field (e.g., appending text). 216 | // KeConcat: 用于更新字段时,将字符串连接到字段(例如追加文本)。 217 | func (columnName ColumnName[TYPE]) KeConcat(x TYPE) (string, clause.Expr) { 218 | return columnName.KeExp(columnName.ExprConcat(x)) 219 | } 220 | 221 | // KeReplace is used in updates where text in the field is replaced (e.g., updating patterns). 222 | // KeReplace: 用于更新字段时,替换字段中的文本(例如更新模式)。 223 | func (columnName ColumnName[TYPE]) KeReplace(oldValue, newValue TYPE) (string, clause.Expr) { 224 | return columnName.KeExp(columnName.ExprReplace(oldValue, newValue)) 225 | } 226 | 227 | // Count creates a COUNT queries statement on the column, excluding NULL values. 228 | // Count: 创建一个 COUNT 查询,只统计非 NULL 值的列。 229 | func (columnName ColumnName[TYPE]) Count(alias string) string { 230 | return utils.ApplyAliasToColumn("COUNT("+string(columnName)+")", alias) 231 | } 232 | 233 | // CountDistinct creates a COUNT DISTINCT queries statement on the given column, skipping NULL values in the count. 234 | // CountDistinct: 创建一个 COUNT DISTINCT 查询,用于给定列,跳过 NULL 值。 235 | func (columnName ColumnName[TYPE]) CountDistinct(alias string) string { 236 | return utils.ApplyAliasToColumn("COUNT(DISTINCT("+string(columnName)+"))", alias) 237 | } 238 | -------------------------------------------------------------------------------- /internal/docs/CREATION_IDEAS.en.md: -------------------------------------------------------------------------------- 1 | # Creative Ideas 2 | 3 | ## Problem 4 | The problem encountered is that when frequently modifying models, the `gorm` logic of `db.Where("name=?", req.Name)` can throw errors at runtime if the `name` field is deleted, renamed, or its type is modified. 5 | 6 | A minor issue is that the word `name` is prone to typos. Although the word itself is unlikely to be misspelled, what if there are many other fields? The most reliable solution often involves copying and pasting, but this can make development less smooth. 7 | 8 | The main issue, however, is that the project may undergo refactoring or requirement changes at any time. The goal is to "ensure models don’t change frequently," but that’s actually a luxury. 9 | 10 | In the early stages of a project, when the requirements are still being defined, frequent changes are common. Later, when there are 20 or more models in the same project, some of which may have a `name` column, deleting a `name` field in one model becomes very cumbersome. This reduces the flexibility of business modifications. For example, due to uncertainty about whether a field is being used, models or database tables can only add fields, not delete them. If the code is full of `Where("name=?", req.Name)`, even the most skilled programmers would be cautious when modifying it. Even if they focus on solving the problem, it's a waste of energy. Our energy is limited, and only by simplifying the basics can we delve deeper into more complex problems. 11 | 12 | ## Solution 13 | At this point, you might have thought about isolating operations related to a model into a separate file, such as creating a `repositories` directory and a `repositories/example.go` file that only operates on the `Example` model. 14 | 15 | This approach does solve many problems. When dealing with database read and write operations in business logic (`service`), you would always call `repositories.FindExampleXxx(x,y,z)` or `repositories.UpdateExampleXxx(x,y,z)`. If there are many models, you could even categorize operations, such as `repositories.ExampleRepo(db).FindXxx(x,y,z)` or `exampleRepo.UpdateXxx(x,y,z)`. 16 | 17 | However, a significant drawback of this approach is that reading the code requires frequent navigation between files. Additionally, some operations only need to be invoked once in a specific scenario, and there’s no need to encapsulate them. But according to the principle mentioned above, they still need to be encapsulated into functions. Furthermore, for slightly more complex queries, the number of parameters increases, leading to function calls like `exampleRepo.UpdateXxx(x,y,z,u,v,w)`. This can be confusing, especially if the arguments are not passed in the correct order. The parameter names must be meaningful, but this can lead to excessively long function names. 18 | 19 | Also, navigating between parameters and arguments becomes cumbersome. 20 | 21 | You might then think of using another ORM like `entgo.io/ent`, which can generate code based on model configurations, providing operations like `func (ec *ExampleCreate) SetName(s string) *ExampleCreate` and `func (eu *ExampleUpdate) SetName(s string) *ExampleUpdate`. This approach ensures static checks during compilation after modifying the model. 22 | 23 | This is indeed a useful package, but the problem is that I don’t like using it. There’s no particular reason for this—probably because I’ve been using `gorm` for so long that I’m not accustomed to using others. New tools always require learning. I only learned the basics of `entgo.io/ent`, and I believe `gorm` is the best. 24 | 25 | Thus, I thought, can I create something based on `gorm`? Initially, I also considered generating code, like using configuration to generate code such as: 26 | ``` 27 | func (G *DemoGorm) Where名字(v string) *DemoGorm { return G.Where名字QS("= ?", v) } 28 | func (G *DemoGorm) Where日期(v string) *DemoGorm { return G.Where日期QS("= ?", v) } 29 | func (G *DemoGorm) Where数值(v float64) *DemoGorm { return G.Where数值QS("= ?", v) } 30 | ``` 31 | But I quickly rejected this solution because it generated hundreds of lines of code for just defining a model, and it still felt clunky to use. Excessive unused code made my project bloated, and the IDE became sluggish. The worst part was that by using my own encapsulated methods, I might forget how to properly use `gorm` or `entgo.io/ent`, which are standard tools—something that could hurt me in job interviews. 32 | 33 | ## Result 34 | Therefore, I gave up on generating code myself. I’m also not good at using `entgo.io/ent`, and not all projects can use it. For instance, if I join a project that doesn't use `entgo.io/ent`, I’m still stuck with `gorm`. 35 | 36 | However, things seem to have changed with the introduction of generics in Go. I suddenly thought that I should generate less code, or even no code at all, but instead provide a third-party package that adds generic capabilities to `gorm` just by importing it. 37 | 38 | ## Expectation 39 | For instance, if my model is: 40 | ``` 41 | type Example struct { 42 | Name string `gorm:"primary_key;type:varchar(100);"` 43 | Type string `gorm:"column:type;"` 44 | Rank int `gorm:"column:rank;"` 45 | } 46 | ``` 47 | I don’t want to do: 48 | ``` 49 | var res Example 50 | err := db.Where("name=?", req.Name).First(&res).Error 51 | ``` 52 | Instead, I want to do: 53 | ``` 54 | var res Example 55 | var cls = res.Columns() // This is a hypothetical operation, not actually available, but I expect something like this to be supported at the Go language level, where the class automatically contains its ORM attribute information 56 | err := db.Where(cls.Name +"=?", req.Name).First(&res).Error // Here, cls contains the metadata (or schema) of Example 57 | ``` 58 | Even deeper thinking: 59 | ``` 60 | var res Example 61 | var cls = res.Columns() 62 | err := db.Where(cls.Name.Eq(req.Name)).First(&res).Error // Let’s truly unleash the potential! If cls.Name.Eq(req.Name) returns ("name=?", req.Name), then it can be directly passed to db.Where() 63 | ``` 64 | Having this expectation, I decided to try and implement it. 65 | 66 | ## Result 67 | To implement the previous expectation, I need to treat "columns" as objects, enabling their generic behavior. First, I focus on imagining the `Eq` function: 68 | ``` 69 | type ColumnName[TYPE any] string 70 | 71 | func (s ColumnName[TYPE]) Eq(x TYPE) (string, TYPE) { 72 | return string(s) + "=?", x 73 | } 74 | ``` 75 | Suppose I define a variable: 76 | ``` 77 | var name = ColumnName[string]("name") // Now I can call name.Eq("abc") to get ("name=?", "abc") 78 | ``` 79 | Because it’s generically defined as a string, using `name.Eq(1)` would result in a compile-time error. 80 | 81 | Next, other operations like `=`, `>`, `<`, `>=`, `<=`, `!=`, `IN`, `BETWEEN...AND...` follow naturally: 82 | ``` 83 | func (s ColumnName[TYPE]) Gt(x TYPE) (string, TYPE) { 84 | return string(s) + ">?", x 85 | } 86 | 87 | func (s ColumnName[TYPE]) Lt(x TYPE) (string, TYPE) { 88 | return string(s) + "100 AND rank<200 ORDER BY `examples`.`name` LIMIT 1 113 | ``` 114 | The result is perfect. 115 | 116 | ## Advantages 117 | I believe this tool has several advantages: 118 | 1. As seen in the self-test code above, you only need to `go get github.com/yyle88/gormcnm`, then define constants/variables to enjoy the convenience this package brings. You can define one or two columns or more if needed. 119 | 2. It doesn’t change `gorm`’s usage or logic, so it doesn’t affect the readability of your code. 120 | 3. Since it follows `gorm`'s query logic, it doesn’t hinder you from learning this powerful tool. This is crucial—tools should not make developers lazy, and using non-standard tools can harm interview performance. 121 | 4. It helps you become more proficient in using `gorm`. 122 | 5. This tool is lightweight. You can choose to use or not use it as you wish. If you haven’t defined the `rank` field constant, you can simply use the original `Where("rank>?", 100)`. This flexibility is very valuable. 123 | 6. Since you can choose whether to use this tool, introducing it into an existing `gorm` project is minimal cost and doesn’t break anything. 124 | 125 | ## Potential of `gormcnm` 126 | 127 | To use this tool, you need to run `go get github.com/yyle88/gormcnm` and `import "github.com/yyle88/gormcnm"`, then define the column constants: 128 | 129 | ```go 130 | const ( 131 | columnName = gormcnm.ColumnName[string]("name") 132 | columnType = gormcnm.ColumnName[string]("type") 133 | columnRank = gormcnm.ColumnName[int]("rank") 134 | ) 135 | ``` 136 | 137 | However, it's obvious that by analyzing the `models` definitions, we can determine which columns exist. In other words, these field definitions **could** potentially be automatically generated through `generate` tools. (This has already been implemented.) 138 | 139 | Moreover, it's clear that these constants don't have a namespace concept. 140 | 141 | For example, what if in another table the `rank` field is no longer an `int`, but instead a `string` with values like "S", "A", "B", "C"? If we define a constant again, it would lead to confusion. 142 | 143 | Therefore, you should do the following: 144 | 145 | ```go 146 | type ExampleColumns struct { 147 | Name gormcnm.ColumnName[string] 148 | Type gormcnm.ColumnName[string] 149 | Rank gormcnm.ColumnName[int] 150 | } 151 | 152 | func (*Example) Columns() *ExampleColumns { 153 | return &ExampleColumns{ 154 | Name: "name", 155 | Type: "type", 156 | Rank: "rank", 157 | } 158 | } 159 | ``` 160 | 161 | Then, you can use it like this: 162 | 163 | ```go 164 | var res models.Example 165 | var cls = res.Columns() 166 | err := db.Where(cls.Name.Eq("abc")). 167 | Where(cls.Type.Eq("xyz")). 168 | Where(cls.Rank.Gt(100)). 169 | Where(cls.Rank.Lt(200)). 170 | First(&res).Error 171 | ``` 172 | 173 | As you can see, the code generated by my tool is exactly like this. 174 | 175 | ```bash 176 | go get github.com/yyle88/gormcngen 177 | ``` 178 | 179 | For usage instructions of this generation tool, please refer to the corresponding project's documentation: [Automatic gormcnm field definition generator pkg gormcngen](https://github.com/yyle88/gormcngen). 180 | 181 | ## Some others 182 | 183 | This project is actually a tribute to `gorm`. However, `gormcnm` might not sound very elegant. If you don't like the name, you can check out [gormrepo](https://github.com/yyle88/gormrepo). 184 | 185 | I occasionally think that the name `gormcls` might be better, especially since when using a Python ORM in the past, I often used `a.cls` for operations. 186 | 187 | However, it’s obvious that this project deals with operations related to column names. Therefore, shortening `gorm column name` to `gormcnm` is just perfect—it’s both loud and elegant, truly fitting for the task. 188 | 189 | ## Conclusion 190 | 191 | This creative background and usage method introduction is now complete. 192 | 193 | Give me stars. Thank you!!! 194 | --------------------------------------------------------------------------------