Skip to content

Commit 5b1fa35

Browse files
authored
Unique args: Handle embedded field that is *not* a struct (#1088)
Aims to fix #1087. One thing I hadn't accounted for is that it's actually possible to have an embedded type that's *not* a struct. e.g. type MyStruct struct { string // embedded non-struct } The simplified example above is *not* because we short circuit unless properties are exported (and `string` is not exported because it starts with a lower case letter), but if we have a similar case with a custom type (e.g. `type MyString string`) then we can reproduce the error described in #1087. Fix the problem by making sure to check that a field is really a struct before recursing back into `getSortedUniqueFields`. We're able to simplify the function somewhat by handling all structs (anonymous or not) in one spot since the logic is so similar. Fixes #1087.
1 parent 21e7153 commit 5b1fa35

3 files changed

Lines changed: 43 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Unique args: Handle embedded fields that are not structs. [PR #1088](https://github.com/riverqueue/river/pull/1088).
13+
1014
## [0.27.0] - 2025-11-14
1115

1216
### Added

internal/dbunique/db_unique_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,22 @@ func TestUniqueKey(t *testing.T) {
200200
uniqueOpts: UniqueOpts{ByArgs: true},
201201
expectedJSON: `&kind=worker_1&args={"recipient":"john@example.com","subject":"Another Test Email"}`,
202202
},
203+
{
204+
name: "ByArgsWithEmbeddedNonStruct",
205+
argsFunc: func() rivertype.JobArgs {
206+
type MyString string
207+
type TaskJobArgs struct {
208+
JobArgsStaticKind
209+
MyString // anonymous non-struct field; needs to be a custom type because it has to be capitalized to be exported
210+
}
211+
return TaskJobArgs{
212+
JobArgsStaticKind: JobArgsStaticKind{kind: "worker_7"},
213+
MyString: "my_string_in_anonymous_field",
214+
}
215+
},
216+
uniqueOpts: UniqueOpts{ByArgs: true},
217+
expectedJSON: `&kind=worker_7&args={"MyString":"my_string_in_anonymous_field"}`,
218+
},
203219
{
204220
name: "ByArgsWithSubstructTagged",
205221
argsFunc: func() rivertype.JobArgs {

internal/dbunique/unique_fields.go

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,6 @@ func getSortedUniqueFields(typ reflect.Type, path []string) ([]string, error) {
6363
continue
6464
}
6565

66-
if field.Anonymous {
67-
uniqueSubFields, err := getSortedUniqueFields(field.Type, path)
68-
if err != nil {
69-
return nil, err
70-
}
71-
uniqueFields = append(uniqueFields, uniqueSubFields...)
72-
73-
continue
74-
}
75-
7666
var uniqueName string
7767
{
7868
// Get the corresponding JSON key
@@ -98,11 +88,20 @@ func getSortedUniqueFields(typ reflect.Type, path []string) ([]string, error) {
9888
}
9989
}
10090

101-
if field.Type.Kind() == reflect.Struct || field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
102-
uniqueSubFields, err := getSortedUniqueFields(field.Type, append(path, uniqueName))
91+
if typeStructOrPointerToStruct(field.Type) {
92+
// Append the JSON to the path (all path segments sent down
93+
// recursively) unless we're looking at an anonymous struct, whose
94+
// fields will be let at the top level.
95+
fullPath := path
96+
if !field.Anonymous {
97+
fullPath = append(path, uniqueName) //nolint:gocritic
98+
}
99+
100+
uniqueSubFields, err := getSortedUniqueFields(field.Type, fullPath)
103101
if err != nil {
104102
return nil, err
105103
}
104+
106105
if len(uniqueSubFields) > 0 {
107106
uniqueFields = append(uniqueFields, uniqueSubFields...)
108107
} else if hasUniqueTag {
@@ -163,3 +162,15 @@ func parseJSONTag(tag string) string {
163162
}
164163
return tag
165164
}
165+
166+
func typeStructOrPointerToStruct(typ reflect.Type) bool {
167+
if typ.Kind() == reflect.Struct {
168+
return true
169+
}
170+
171+
if typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct {
172+
return true
173+
}
174+
175+
return false
176+
}

0 commit comments

Comments
 (0)