From be274aedb66b9f99f3687be983ce172c82b213a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 08:36:19 +0000 Subject: [PATCH] chore(deps): Bump github.com/jacobbrewer1/patcher from 0.1.6 to 0.1.9 (#58) Bumps [github.com/jacobbrewer1/patcher](https://github.com/jacobbrewer1/patcher) from 0.1.6 to 0.1.9. - [Release notes](https://github.com/jacobbrewer1/patcher/releases) - [Commits](https://github.com/jacobbrewer1/patcher/compare/v0.1.6...v0.1.9) --- updated-dependencies: - dependency-name: github.com/jacobbrewer1/patcher dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 +- .../jacobbrewer1/patcher/.clog.toml | 2 +- .../github.com/jacobbrewer1/patcher/Makefile | 2 + .../github.com/jacobbrewer1/patcher/README.md | 77 +++++++++++++++-- .../jacobbrewer1/patcher/inserter/batch.go | 27 ++++++ .../patcher/inserter/batch_opts.go | 20 ++++- .../jacobbrewer1/patcher/inserter/sql.go | 72 ++++++++++++++-- .../github.com/jacobbrewer1/patcher/loader.go | 65 ++++++++++++--- .../patcher/mock_IgnoreFieldsFunc.go | 46 ++++++++++ .../github.com/jacobbrewer1/patcher/patch.go | 51 +++++++++++- .../jacobbrewer1/patcher/patch_opts.go | 58 +++++++++++-- vendor/github.com/jacobbrewer1/patcher/sql.go | 83 ++++++++++++------- vendor/modules.txt | 2 +- 14 files changed, 439 insertions(+), 72 deletions(-) create mode 100644 vendor/github.com/jacobbrewer1/patcher/mock_IgnoreFieldsFunc.go diff --git a/go.mod b/go.mod index 0add4be..c3cfaf6 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/google/subcommands v1.2.0 github.com/gorilla/mux v1.8.1 github.com/jacobbrewer1/pagefilter v0.1.4 - github.com/jacobbrewer1/patcher v0.1.6 + github.com/jacobbrewer1/patcher v0.1.9 github.com/jacobbrewer1/uhttp v0.0.4 github.com/jacobbrewer1/vaulty v0.1.3 github.com/jacobbrewer1/workerpool v0.0.2 diff --git a/go.sum b/go.sum index f3c8bc1..4d5d38d 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jacobbrewer1/pagefilter v0.1.4 h1:yo+F4qsmu1DHo0mIcoBHeofIufO0HnUW7/9eaqN0K1s= github.com/jacobbrewer1/pagefilter v0.1.4/go.mod h1:6tTrmLumsrhUuDuvOSU8PEFP3n1IXifeAt+A3+Pr9jk= -github.com/jacobbrewer1/patcher v0.1.6 h1:XTmNbR1caXysvZoUN1VzaLJfzzuqgRubcFkjawY2v28= -github.com/jacobbrewer1/patcher v0.1.6/go.mod h1:ykwtddQ1pG18aFBRsfuIGNSkADNCW6XnN49rbeqP1ZY= +github.com/jacobbrewer1/patcher v0.1.9 h1:tefNIK4l3lOYnHlG5VeT4xLYsyfZNQPAOlBrNhMeOIE= +github.com/jacobbrewer1/patcher v0.1.9/go.mod h1:VsVaZhwRCQpmKu1kPfV0qf35xRdjE8ecdkNrb0RrK3U= github.com/jacobbrewer1/uhttp v0.0.4 h1:3RrRmz0fjsnLKuAs5fkflOcsjuBFoL0XvC6Xct/TDCk= github.com/jacobbrewer1/uhttp v0.0.4/go.mod h1:qwLAdGw4SLYJY2MS0sE1m/w1ILTr61bjwRtlbGwqo5c= github.com/jacobbrewer1/vaulty v0.1.3 h1:0L3JnmzqcxL91COVMUkT+HWje3jTy9YjYbpFOyGmHBo= diff --git a/vendor/github.com/jacobbrewer1/patcher/.clog.toml b/vendor/github.com/jacobbrewer1/patcher/.clog.toml index 8bbb4fb..c3585b6 100644 --- a/vendor/github.com/jacobbrewer1/patcher/.clog.toml +++ b/vendor/github.com/jacobbrewer1/patcher/.clog.toml @@ -1,7 +1,7 @@ [clog] # A repository link with the trailing '.git' which will be used to generate # all commit and issue links -repository = "https://github.com/Jacobbrewer1/patcher" +repository = "https://github.com/jacobbrewer1/patcher" # A constant release title subtitle = "What's Changed" diff --git a/vendor/github.com/jacobbrewer1/patcher/Makefile b/vendor/github.com/jacobbrewer1/patcher/Makefile index 65e463e..eeaa30f 100644 --- a/vendor/github.com/jacobbrewer1/patcher/Makefile +++ b/vendor/github.com/jacobbrewer1/patcher/Makefile @@ -7,3 +7,5 @@ pr-approval: go build ./... go vet ./... go test ./... +codegen: + go generate ./... diff --git a/vendor/github.com/jacobbrewer1/patcher/README.md b/vendor/github.com/jacobbrewer1/patcher/README.md index dbd95dd..6b0951a 100644 --- a/vendor/github.com/jacobbrewer1/patcher/README.md +++ b/vendor/github.com/jacobbrewer1/patcher/README.md @@ -23,13 +23,12 @@ import ( "encoding/json" "fmt" - "github.com/Jacobbrewer1/patcher" + "github.com/jacobbrewer1/patcher" ) type Person struct { - ID *int `db:"id"` + ID *int `db:"-"` Name *string `db:"name"` - Age *int `db:"age"` } type PersonWhere struct { @@ -47,14 +46,14 @@ func (p *PersonWhere) Where() (string, []any) { } func main() { - const jsonStr = `{"name": "John", "age": 25}` + const jsonStr = `{"id": 1, "name": "john"}` person := new(Person) if err := json.Unmarshal([]byte(jsonStr), person); err != nil { panic(err) } - condition := NewPersonWhere(1) + condition := NewPersonWhere(*person.ID) sqlStr, args, err := patcher.GenerateSQL( person, @@ -65,9 +64,20 @@ func main() { panic(err) } + // Output: + // UPDATE people + // SET name = ? + // WHERE (1 = 1) + // AND ( + // id = ? + // ) fmt.Println(sqlStr) + + // Output: + // ["John", 1] fmt.Println(args) } + ``` This will output: @@ -90,6 +100,61 @@ with the args: #### Struct diffs +The Patcher library has functionality where you are able to inject changes from one struct to another. This is +configurable to include Zero values and Nil values if requested. Please see the +example [here](./examples/loader_with_opts) for the detailed example. Below is an example on how you can utilize this +method with the default behaviour (Please see the comment attached to the `LoadDiff` [method](./loader.go) for the +default behaviour). + +Example: + +```go +package main + +import ( + "fmt" + + "github.com/jacobbrewer1/patcher" +) + +type Something struct { + Number int + Text string + PrePopulated string + NewText string +} + +func main() { + s := Something{ + Number: 5, + Text: "Hello", + PrePopulated: "PrePopulated", + } + + n := Something{ + Number: 6, + Text: "Old Text", + NewText: "New Text", + } + + // The patcher.LoadDiff function will apply the changes from n to s. + if err := patcher.LoadDiff(&s, &n); err != nil { + panic(err) + } + + // Output: + // 6 + // Old Text + // PrePopulated + // New Text + fmt.Println(s.Number) + fmt.Println(s.Text) + fmt.Println(s.PrePopulated) + fmt.Println(s.NewText) +} + +``` + If you would like to generate an update script from two structs, you can use the `NewDiffSQLPatch` function. This function will generate an update script from the two structs. @@ -101,7 +166,7 @@ package main import ( "fmt" - "github.com/Jacobbrewer1/patcher" + "github.com/jacobbrewer1/patcher" ) type Something struct { diff --git a/vendor/github.com/jacobbrewer1/patcher/inserter/batch.go b/vendor/github.com/jacobbrewer1/patcher/inserter/batch.go index 6f2a55b..3a22047 100644 --- a/vendor/github.com/jacobbrewer1/patcher/inserter/batch.go +++ b/vendor/github.com/jacobbrewer1/patcher/inserter/batch.go @@ -3,6 +3,8 @@ package inserter import ( "database/sql" "errors" + + "github.com/jacobbrewer1/patcher" ) var ( @@ -34,6 +36,31 @@ type SQLBatch struct { // table is the table name to use in the SQL statement table string + + // ignoreFields is a list of fields to ignore when patching + ignoreFields []string + + // ignoreFieldsFunc is a function that determines whether a field should be ignored + // + // This func should return true is the field is to be ignored + ignoreFieldsFunc patcher.IgnoreFieldsFunc +} + +// newBatchDefaults returns a new SQLBatch with default values +func newBatchDefaults(opts ...BatchOpt) *SQLBatch { + b := &SQLBatch{ + fields: nil, + args: nil, + db: nil, + tagName: patcher.DefaultDbTagName, + table: "", + } + + for _, opt := range opts { + opt(b) + } + + return b } func (b *SQLBatch) Fields() []string { diff --git a/vendor/github.com/jacobbrewer1/patcher/inserter/batch_opts.go b/vendor/github.com/jacobbrewer1/patcher/inserter/batch_opts.go index 4a7e2f5..039b17c 100644 --- a/vendor/github.com/jacobbrewer1/patcher/inserter/batch_opts.go +++ b/vendor/github.com/jacobbrewer1/patcher/inserter/batch_opts.go @@ -1,6 +1,10 @@ package inserter -import "database/sql" +import ( + "database/sql" + + "github.com/jacobbrewer1/patcher" +) type BatchOpt func(*SQLBatch) @@ -24,3 +28,17 @@ func WithDB(db *sql.DB) BatchOpt { b.db = db } } + +// WithIgnoreFields sets the fields to ignore when patching +func WithIgnoreFields(fields ...string) BatchOpt { + return func(b *SQLBatch) { + b.ignoreFields = fields + } +} + +// WithIgnoreFieldsFunc sets the function that determines whether a field should be ignored +func WithIgnoreFieldsFunc(f patcher.IgnoreFieldsFunc) BatchOpt { + return func(b *SQLBatch) { + b.ignoreFieldsFunc = f + } +} diff --git a/vendor/github.com/jacobbrewer1/patcher/inserter/sql.go b/vendor/github.com/jacobbrewer1/patcher/inserter/sql.go index b9fc8d4..659a9a2 100644 --- a/vendor/github.com/jacobbrewer1/patcher/inserter/sql.go +++ b/vendor/github.com/jacobbrewer1/patcher/inserter/sql.go @@ -4,17 +4,15 @@ import ( "database/sql" "fmt" "reflect" + "slices" "strings" -) -const ( - // defaultTagName is the default tag name to look for in the struct - defaultTagName = "db" + "github.com/jacobbrewer1/patcher" ) func NewBatch(resources []any, opts ...BatchOpt) *SQLBatch { - b := new(SQLBatch) - b.tagName = defaultTagName + b := newBatchDefaults(opts...) + for _, opt := range opts { opt(b) } @@ -49,20 +47,34 @@ func (b *SQLBatch) genBatch(resources []any) { for i := 0; i < t.NumField(); i++ { f := t.Field(i) tag := f.Tag.Get(b.tagName) - if tag == "-" { + if tag == patcher.TagOptSkip { continue } + // Skip unexported fields if !f.IsExported() { continue } + // Skip fields that are to be ignored + if b.checkSkipField(f) { + continue + } + + patcherOptsTag := f.Tag.Get(patcher.TagOptsName) + if patcherOptsTag != "" { + patcherOpts := strings.Split(patcherOptsTag, patcher.TagOptSeparator) + if slices.Contains(patcherOpts, patcher.TagOptSkip) { + continue + } + } + // if no tag is set, use the field name if tag == "" { - tag = strings.ToLower(f.Name) + tag = f.Name } - b.args = append(b.args, v.Field(i).Interface()) + b.args = append(b.args, b.getFieldValue(v.Field(i), f)) // if the field is not unique, skip it if _, ok := uniqueFields[tag]; ok { @@ -76,6 +88,17 @@ func (b *SQLBatch) genBatch(resources []any) { } } +func (b *SQLBatch) getFieldValue(v reflect.Value, f reflect.StructField) any { + if f.Type.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + return v.Elem().Interface() + } + + return v.Interface() +} + func (b *SQLBatch) GenerateSQL() (string, []any, error) { if err := b.validateSQLGen(); err != nil { return "", nil, err @@ -116,3 +139,34 @@ func (b *SQLBatch) Perform() (sql.Result, error) { return b.db.Exec(sqlStr, args...) } + +func (b *SQLBatch) checkSkipField(field reflect.StructField) bool { + // The ignore fields tag takes precedence over the ignore fields list + if b.checkSkipTag(field) { + return true + } + + return b.ignoredFieldsCheck(field) +} + +func (b *SQLBatch) checkSkipTag(field reflect.StructField) bool { + val, ok := field.Tag.Lookup(patcher.TagOptsName) + if !ok { + return false + } + + tags := strings.Split(val, patcher.TagOptSeparator) + return slices.Contains(tags, patcher.TagOptSkip) +} + +func (b *SQLBatch) ignoredFieldsCheck(field reflect.StructField) bool { + return b.checkIgnoredFields(strings.ToLower(field.Name)) || b.checkIgnoreFunc(field) +} + +func (b *SQLBatch) checkIgnoreFunc(field reflect.StructField) bool { + return b.ignoreFieldsFunc != nil && b.ignoreFieldsFunc(field) +} + +func (b *SQLBatch) checkIgnoredFields(field string) bool { + return len(b.ignoreFields) > 0 && slices.Contains(b.ignoreFields, strings.ToLower(field)) +} diff --git a/vendor/github.com/jacobbrewer1/patcher/loader.go b/vendor/github.com/jacobbrewer1/patcher/loader.go index 36fbcfd..0f2e077 100644 --- a/vendor/github.com/jacobbrewer1/patcher/loader.go +++ b/vendor/github.com/jacobbrewer1/patcher/loader.go @@ -3,6 +3,8 @@ package patcher import ( "errors" "reflect" + "slices" + "strings" ) var ( @@ -11,17 +13,19 @@ var ( ) // LoadDiff inserts the fields provided in the new struct pointer into the old struct pointer and injects the new -// values into the old struct. +// values into the old struct // // Note that it only pushes non-zero value updates, meaning you cannot set any field to zero, the empty string, etc. +// This is configurable by setting the includeZeroValues option to true or for nil values by setting includeNilValues. +// Please see the LoaderOption's for more configuration options. // // This can be if you are inserting a patch into an existing object but require a new object to be returned with -// all fields. -func LoadDiff[T any](old *T, newT *T) error { - return loadDiff(old, newT) +// all fields +func LoadDiff[T any](old *T, newT *T, opts ...PatchOpt) error { + return newPatchDefaults(opts...).loadDiff(old, newT) } -func loadDiff[T any](old T, newT T) error { +func (s *SQLPatch) loadDiff(old, newT any) error { if !isPointerToStruct(old) || !isPointerToStruct(newT) { return ErrInvalidType } @@ -39,8 +43,8 @@ func loadDiff[T any](old T, newT T) error { if oElem.Type().Field(i).Anonymous { // If the embedded field is a pointer, dereference it if oElem.Field(i).Kind() == reflect.Ptr { - if !oElem.Field(i).IsNil() && !nElem.Field(i).IsNil() { - if err := loadDiff(oElem.Field(i).Interface(), nElem.Field(i).Interface()); err != nil { + if !oElem.Field(i).IsNil() && !nElem.Field(i).IsNil() { // If both are not nil, we need to recursively call LoadDiff + if err := s.loadDiff(oElem.Field(i).Interface(), nElem.Field(i).Interface()); err != nil { return err } } else if nElem.Field(i).IsValid() && !nElem.Field(i).IsNil() { @@ -50,7 +54,7 @@ func loadDiff[T any](old T, newT T) error { continue } - if err := loadDiff(oElem.Field(i).Addr().Interface(), nElem.Field(i).Addr().Interface()); err != nil { + if err := s.loadDiff(oElem.Field(i).Addr().Interface(), nElem.Field(i).Addr().Interface()); err != nil { return err } continue @@ -58,20 +62,57 @@ func loadDiff[T any](old T, newT T) error { // If the field is a struct, we need to recursively call LoadDiff if oElem.Field(i).Kind() == reflect.Struct { - if err := loadDiff(oElem.Field(i).Addr().Interface(), nElem.Field(i).Addr().Interface()); err != nil { + if err := s.loadDiff(oElem.Field(i).Addr().Interface(), nElem.Field(i).Addr().Interface()); err != nil { return err } continue } + // See if the field should be ignored. + if s.checkSkipField(oElem.Type().Field(i)) { + continue + } + // Compare the old and new fields. // - // New fields take priority over old fields if they are provided. We ignore zero values as they are not - // provided in the new object. - if !nElem.Field(i).IsZero() { + // New fields take priority over old fields if they are provided based on the configuration. + if nElem.Field(i).Kind() != reflect.Ptr && (!nElem.Field(i).IsZero() || s.includeZeroValues) { + oElem.Field(i).Set(nElem.Field(i)) + } else if nElem.Field(i).Kind() == reflect.Ptr && (!nElem.Field(i).IsNil() || s.includeNilValues) { oElem.Field(i).Set(nElem.Field(i)) } } return nil } + +func (s *SQLPatch) checkSkipField(field reflect.StructField) bool { + // The ignore fields tag takes precedence over the ignore fields list + if s.checkSkipTag(field) { + return true + } + + return s.ignoredFieldsCheck(field) +} + +func (s *SQLPatch) checkSkipTag(field reflect.StructField) bool { + val, ok := field.Tag.Lookup(TagOptsName) + if !ok { + return false + } + + tags := strings.Split(val, TagOptSeparator) + return slices.Contains(tags, TagOptSkip) +} + +func (s *SQLPatch) ignoredFieldsCheck(field reflect.StructField) bool { + return s.checkIgnoredFields(strings.ToLower(field.Name)) || s.checkIgnoreFunc(field) +} + +func (s *SQLPatch) checkIgnoreFunc(field reflect.StructField) bool { + return s.ignoreFieldsFunc != nil && s.ignoreFieldsFunc(field) +} + +func (s *SQLPatch) checkIgnoredFields(field string) bool { + return len(s.ignoreFields) > 0 && slices.Contains(s.ignoreFields, strings.ToLower(field)) +} diff --git a/vendor/github.com/jacobbrewer1/patcher/mock_IgnoreFieldsFunc.go b/vendor/github.com/jacobbrewer1/patcher/mock_IgnoreFieldsFunc.go new file mode 100644 index 0000000..7781a40 --- /dev/null +++ b/vendor/github.com/jacobbrewer1/patcher/mock_IgnoreFieldsFunc.go @@ -0,0 +1,46 @@ +// Code generated by mockery. DO NOT EDIT. + +package patcher + +import ( + reflect "reflect" + + mock "github.com/stretchr/testify/mock" +) + +// MockIgnoreFieldsFunc is an autogenerated mock type for the IgnoreFieldsFunc type +type MockIgnoreFieldsFunc struct { + mock.Mock +} + +// Execute provides a mock function with given fields: field +func (_m *MockIgnoreFieldsFunc) Execute(field reflect.StructField) bool { + ret := _m.Called(field) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(reflect.StructField) bool); ok { + r0 = rf(field) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// NewMockIgnoreFieldsFunc creates a new instance of MockIgnoreFieldsFunc. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockIgnoreFieldsFunc(t interface { + mock.TestingT + Cleanup(func()) +}) *MockIgnoreFieldsFunc { + mock := &MockIgnoreFieldsFunc{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/vendor/github.com/jacobbrewer1/patcher/patch.go b/vendor/github.com/jacobbrewer1/patcher/patch.go index df5cc42..4196677 100644 --- a/vendor/github.com/jacobbrewer1/patcher/patch.go +++ b/vendor/github.com/jacobbrewer1/patcher/patch.go @@ -3,6 +3,7 @@ package patcher import ( "database/sql" "errors" + "reflect" "strings" ) @@ -23,6 +24,8 @@ var ( ErrNoWhere = errors.New("no where clause set") ) +type IgnoreFieldsFunc func(field reflect.StructField) bool + type SQLPatch struct { // fields is the fields to update in the SQL statement fields []string @@ -40,16 +43,56 @@ type SQLPatch struct { table string // whereSql is the where clause to use in the SQL statement - where strings.Builder + whereSql *strings.Builder // whereArgs is the arguments to use in the where clause whereArgs []any // joinSql is the join clause to use in the SQL statement - joinSql strings.Builder + joinSql *strings.Builder // joinArgs is the arguments to use in the join clause joinArgs []any + + // includeZeroValues determines whether zero values should be included in the patch + includeZeroValues bool + + // includeNilValues determines whether nil values should be included in the patch + includeNilValues bool + + // ignoreFields is a list of fields to ignore when patching + ignoreFields []string + + // ignoreFieldsFunc is a function that determines whether a field should be ignored + // + // This func should return true is the field is to be ignored + ignoreFieldsFunc IgnoreFieldsFunc +} + +// newPatchDefaults creates a new SQLPatch with default options. +func newPatchDefaults(opts ...PatchOpt) *SQLPatch { + // Default options + p := &SQLPatch{ + fields: nil, + args: nil, + db: nil, + tagName: DefaultDbTagName, + table: "", + whereSql: new(strings.Builder), + whereArgs: nil, + joinSql: new(strings.Builder), + joinArgs: nil, + includeZeroValues: false, + includeNilValues: false, + ignoreFields: nil, + ignoreFieldsFunc: nil, + } + + for _, opt := range opts { + opt(p) + } + + return p } func (s *SQLPatch) Fields() []string { @@ -69,7 +112,7 @@ func (s *SQLPatch) validatePerformPatch() error { return ErrNoFields } else if len(s.args) == 0 { return ErrNoArgs - } else if s.where.String() == "" { + } else if s.whereSql.String() == "" { return ErrNoWhere } @@ -83,7 +126,7 @@ func (s *SQLPatch) validateSQLGen() error { return ErrNoFields } else if len(s.args) == 0 { return ErrNoArgs - } else if s.where.String() == "" { + } else if s.whereSql.String() == "" { return ErrNoWhere } diff --git a/vendor/github.com/jacobbrewer1/patcher/patch_opts.go b/vendor/github.com/jacobbrewer1/patcher/patch_opts.go index 87907f4..a2a16f7 100644 --- a/vendor/github.com/jacobbrewer1/patcher/patch_opts.go +++ b/vendor/github.com/jacobbrewer1/patcher/patch_opts.go @@ -3,8 +3,12 @@ package patcher import ( "database/sql" "strings" +) - "github.com/jmoiron/sqlx" +const ( + TagOptsName = "patcher" + TagOptSeparator = "," + TagOptSkip = "-" ) type PatchOpt func(*SQLPatch) @@ -26,6 +30,9 @@ func WithTable(table string) PatchOpt { // WithWhere sets the where clause to use in the SQL statement func WithWhere(where Wherer) PatchOpt { return func(s *SQLPatch) { + if s.whereSql == nil { + s.whereSql = new(strings.Builder) + } fwSQL, fwArgs := where.Where() if fwArgs == nil { fwArgs = []any{} @@ -35,9 +42,9 @@ func WithWhere(where Wherer) PatchOpt { if ok && wt.WhereType().IsValid() { wtStr = wt.WhereType() } - s.where.WriteString(string(wtStr) + " ") - s.where.WriteString(strings.TrimSpace(fwSQL)) - s.where.WriteString("\n") + s.whereSql.WriteString(string(wtStr) + " ") + s.whereSql.WriteString(strings.TrimSpace(fwSQL)) + s.whereSql.WriteString("\n") s.whereArgs = append(s.whereArgs, fwArgs...) } } @@ -45,6 +52,9 @@ func WithWhere(where Wherer) PatchOpt { // WithJoin sets the join clause to use in the SQL statement func WithJoin(join Joiner) PatchOpt { return func(s *SQLPatch) { + if s.joinSql == nil { + s.joinSql = new(strings.Builder) + } fjSQL, fjArgs := join.Join() if fjArgs == nil { fjArgs = []any{} @@ -62,9 +72,43 @@ func WithDB(db *sql.DB) PatchOpt { } } -// WithSQLxDB sets the database from an SQLx connection -func WithSQLxDB(db *sqlx.DB) PatchOpt { +// WithIncludeZeroValues sets whether zero values should be included in the patch. +// +// This is useful when you want to set a field to zero. +func WithIncludeZeroValues() PatchOpt { + return func(s *SQLPatch) { + s.includeZeroValues = true + } +} + +// WithIncludeNilValues sets whether nil values should be included in the patch. +// +// This is useful when you want to set a field to nil. +func WithIncludeNilValues() PatchOpt { + return func(s *SQLPatch) { + s.includeNilValues = true + } +} + +// WithIgnoredFields sets the fields to ignore when patching. +// +// This should be the actual field name, not the JSON tag name or the db tag name. +// +// Note. When we parse the slice of strings, we convert them to lowercase to ensure that the comparison is +// case-insensitive. +func WithIgnoredFields(fields ...string) PatchOpt { + return func(s *SQLPatch) { + for i := range fields { + fields[i] = strings.ToLower(fields[i]) + } + + s.ignoreFields = fields + } +} + +// WithIgnoredFieldsFunc sets a function that determines whether a field should be ignored when patching. +func WithIgnoredFieldsFunc(f IgnoreFieldsFunc) PatchOpt { return func(s *SQLPatch) { - s.db = db.DB + s.ignoreFieldsFunc = f } } diff --git a/vendor/github.com/jacobbrewer1/patcher/sql.go b/vendor/github.com/jacobbrewer1/patcher/sql.go index 58ca02e..a725e4d 100644 --- a/vendor/github.com/jacobbrewer1/patcher/sql.go +++ b/vendor/github.com/jacobbrewer1/patcher/sql.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" "reflect" + "slices" "strings" ) -const tagName = "db" +const ( + DefaultDbTagName = "db" +) var ( // ErrNoChanges is returned when no changes are detected between the old and new objects @@ -16,15 +19,8 @@ var ( ) func NewSQLPatch(resource any, opts ...PatchOpt) *SQLPatch { - sqlPatch := new(SQLPatch) - sqlPatch.tagName = tagName - - for _, opt := range opts { - opt(sqlPatch) - } - + sqlPatch := newPatchDefaults(opts...) sqlPatch.patchGen(resource) - return sqlPatch } @@ -52,22 +48,48 @@ func (s *SQLPatch) patchGen(resource any) { fVal := rVal.Field(i) tag := fType.Tag.Get(s.tagName) - // skip nil properties (not going to be patched), skip unexported fields, skip fields to be skipped for SQL - if fVal.Kind() == reflect.Ptr && (fVal.IsNil() || fType.PkgPath != "" || tag == "-") { + // Skip unexported fields + if !fType.IsExported() { continue - } else if fVal.Kind() != reflect.Ptr && fVal.IsZero() { - // skip zero values for non-pointer fields as we have no way to differentiate between zero values and nil pointers + } + + // Skip fields that are to be ignored + if s.checkSkipField(fType) { + continue + } else if fVal.Kind() == reflect.Ptr && (fVal.IsNil() && !s.includeNilValues) { + continue + } else if fVal.Kind() != reflect.Ptr && (fVal.IsZero() && !s.includeZeroValues) { continue } + patcherOptsTag := fType.Tag.Get(TagOptsName) + if patcherOptsTag != "" { + patcherOpts := strings.Split(patcherOptsTag, TagOptSeparator) + if slices.Contains(patcherOpts, TagOptSkip) { + continue + } + } + // if no tag is set, use the field name - if tag == "" { - tag = strings.ToLower(fType.Name) + if tag == TagOptSkip { + continue + } else if tag == "" { + tag = fType.Name } - // and make the tag lowercase in the end - tag = strings.ToLower(tag) - s.fields = append(s.fields, tag+" = ?") + addField := func() { + s.fields = append(s.fields, tag+" = ?") + } + + if fVal.Kind() == reflect.Ptr && fVal.IsNil() && s.includeNilValues { + s.args = append(s.args, nil) + addField() + continue + } else if fVal.Kind() == reflect.Ptr && fVal.IsNil() { + continue + } + + addField() var val reflect.Value if fVal.Kind() == reflect.Ptr { @@ -87,9 +109,11 @@ func (s *SQLPatch) patchGen(resource any) { boolArg = 1 } s.args = append(s.args, boolArg) + case reflect.Float32, reflect.Float64: + s.args = append(s.args, val.Float()) default: // This is intentionally a panic as this is a programming error and should be fixed by the developer - panic("unhandled default case") + panic(fmt.Sprintf("unsupported type: %s", val.Kind())) } } } @@ -121,7 +145,7 @@ func (s *SQLPatch) GenerateSQL() (string, []any, error) { sqlBuilder.WriteString("AND (\n") // If the where clause starts with "AND" or "OR", we need to remove it - where := s.where.String() + where := s.whereSql.String() if strings.HasPrefix(where, string(WhereTypeAnd)) || strings.HasPrefix(where, string(WhereTypeOr)) { where = strings.TrimPrefix(where, string(WhereTypeAnd)) where = strings.TrimPrefix(where, string(WhereTypeOr)) @@ -175,7 +199,8 @@ func NewDiffSQLPatch[T any](old, newT *T, opts ...PatchOpt) (*SQLPatch, error) { // copy the old object into the copy reflect.ValueOf(oldCopy).Elem().Set(reflect.ValueOf(old).Elem()) - if err := LoadDiff(old, newT); err != nil { + patch := newPatchDefaults(opts...) + if err := patch.loadDiff(old, newT); err != nil { return nil, fmt.Errorf("load diff: %w", err) } @@ -189,21 +214,23 @@ func NewDiffSQLPatch[T any](old, newT *T, opts ...PatchOpt) (*SQLPatch, error) { oldField := reflect.ValueOf(old).Elem().Field(i) copyField := reflect.ValueOf(oldCopy).Elem().Field(i) - if oldField.Kind() == reflect.Ptr && oldField.IsNil() { + if oldField.Kind() == reflect.Ptr && (oldField.IsNil() && copyField.IsNil() && !patch.includeZeroValues) { continue - } else if oldField.Kind() != reflect.Ptr && oldField.IsZero() { + } else if oldField.Kind() != reflect.Ptr && (oldField.IsZero() && copyField.IsZero() && !patch.includeZeroValues) { continue } if reflect.DeepEqual(oldField.Interface(), copyField.Interface()) { - if oldField.Kind() == reflect.Ptr { - oldField.Set(reflect.Zero(oldField.Type())) - continue + // Field is the same, set it to zero or nil. Add it to be ignored in the patch + if patch.ignoreFields == nil { + patch.ignoreFields = make([]string, 0) } - oldField.Set(reflect.New(oldField.Type()).Elem()) + patch.ignoreFields = append(patch.ignoreFields, strings.ToLower(reflect.ValueOf(old).Elem().Type().Field(i).Name)) continue } } - return NewSQLPatch(old, opts...), nil + patch.patchGen(old) + + return patch, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 670cdcb..d242e9e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -146,7 +146,7 @@ github.com/inconshreveable/mousetrap ## explicit; go 1.23 github.com/jacobbrewer1/pagefilter github.com/jacobbrewer1/pagefilter/common -# github.com/jacobbrewer1/patcher v0.1.6 +# github.com/jacobbrewer1/patcher v0.1.9 ## explicit; go 1.22 github.com/jacobbrewer1/patcher github.com/jacobbrewer1/patcher/inserter