Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GODRIVER-2443 Make Distinct return a decodable struct #1603

Merged

Conversation

prestonvasquez
Copy link
Collaborator

@prestonvasquez prestonvasquez commented Apr 12, 2024

GODRIVER-2443

Summary

The Distinct collection method should return a struct that can be decoded, similar to FindOne.

Background & Motivation

Providing a decodable type will improve user experience. Instead of iterating through an any slice, users can decode same-type data using conventional Go syntax:

res, err := coll.Distinct(ctx, "myField", filter)
if err != nil {
	return err
}

var arr []string
res.Decode(&arr)

Alternatively, if the data returned is not same-type a user can iterate it directly as a bson.RawArray type:

res, err := coll.Distinct(ctx, "myField", filter)
if err != nil {
	return err
}

val, err := res.Values()
if err != nil {
	return err
}

for _, rawValue := range val {
	switch rawValue.Type {
		case bson.TypeDouble:
			var f64 float64 
			
			rawValue.Unmarshal(&f64)
			fmt.Println("f64: ", f64)
		case bson.TypeString:
			var str string 
			
			rawValue.Unmarshal(&str)
			fmt.Println("str: ", str)
	}
}

@mongodb-drivers-pr-bot mongodb-drivers-pr-bot bot added the priority-3-low Low Priority PR for Review label Apr 12, 2024
Copy link

mongodb-drivers-pr-bot bot commented Apr 12, 2024

API Change Report

./mongo

incompatible changes

##(Collection).Distinct: changed from func(context.Context, string, interface{}, ..../mongo/options.DistinctOptions) ([]interface{}, error) to func(context.Context, string, interface{}, ...*./mongo/options.DistinctOptions) *DistinctResult

compatible changes

DistinctResult: added

@@ -22,11 +22,11 @@ var ErrNoDocuments = errors.New("mongo: no documents in result")
// SingleResult represents a single document returned from an operation. If the operation resulted in an error, all
// SingleResult methods will return that error. If the operation did not return any documents, all SingleResult methods
// will return ErrNoDocuments.
type SingleResult struct {
type SingleResult[T bson.Raw | bson.RawArray] struct {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T defines the return type of the SingleResult.Raw method.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about the scope of the change required to add this type parameter. One of the challenges with generic types is that they require parameterizing all functions and types that use them, unless only a single type is ever used (see verifySingleResult).

An alternate approach that doesn't impact any existing types is to add a new type DistinctResult that holds a SingleResult and mostly reproduces the API of SingleResult, changing the return type of Raw. That creates fewer parameterized type changes in existing code and provides the ability to independently extend the APIs of DistinctResult and SingleResult.

E.g.

type DistinctResult struct {
	sr *SingleResult
}

func (dr *DistinctResult) Decode(v interface{}) error { return dr.sr.Decode(v) }
func (dr *DistinctResult) Err() error                 { return dr.sr.Err() }

func (dr *DistinctResult) Raw() (bson.RawArray, error) {
	r, err := dr.sr.Raw()
	return bson.RawArray(r), err
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some examples of user code that would be impacted by adding a type parameter to SingleResult. They seem to fall in a few categories:

  • Database client wrappers that return a *mongo.SingleResult.
  • Interfaces used for mocking the Collection API.
  • Adding helper functions for using a *mongo.SingleResult.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see what you mean. It's worth noting that most of these examples will require updates due to GODRIVER-2696. However, I believe it's reasonable to minimize the impact where possible

@prestonvasquez prestonvasquez marked this pull request as ready for review April 12, 2024 22:58
Copy link
Collaborator

@matthewdale matthewdale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the API of SingleResult seems to work great for decoding results from Distinct. I have concerns about the parameterized type.

return dec.Decode(v)
}

return sr.rdr.Unmarshal(v)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use the configured Registry and BSONOptions when unmarshaling the RawValue here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to decode distinct data with a registry, but the BSONOptions are specific to bson.Decoder which expects the value to decode into to be a map / struct. E.g., the following would throw a runtime error:

var i32s []int32
distincResult.Decode(i32s)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct, there's currently no way to do that. However, when combined with the changes in #1584, the following code using a bson.Decoder to decode a value works:

func (dr *DistinctResult) Decode(v any) error {
	doc := bsoncore.NewDocumentBuilder().
		AppendValue("Arr", bsoncore.Value{
			Type: bsoncore.TypeArray,
			Data: dr.arr,
		}).Build()

	dec := getDecoder(doc, dr.opts, dr.reg)

	s := struct {
		Arr any
	}{
		Arr: v,
	}
	return dec.Decode(&s)
}

I created GODRIVER-3205 and added it to the Go Driver 2.0 epic to track that improvement, so we can consider that is resolved for now.

mongo/collection.go Outdated Show resolved Hide resolved
@@ -22,11 +22,11 @@ var ErrNoDocuments = errors.New("mongo: no documents in result")
// SingleResult represents a single document returned from an operation. If the operation resulted in an error, all
// SingleResult methods will return that error. If the operation did not return any documents, all SingleResult methods
// will return ErrNoDocuments.
type SingleResult struct {
type SingleResult[T bson.Raw | bson.RawArray] struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about the scope of the change required to add this type parameter. One of the challenges with generic types is that they require parameterizing all functions and types that use them, unless only a single type is ever used (see verifySingleResult).

An alternate approach that doesn't impact any existing types is to add a new type DistinctResult that holds a SingleResult and mostly reproduces the API of SingleResult, changing the return type of Raw. That creates fewer parameterized type changes in existing code and provides the ability to independently extend the APIs of DistinctResult and SingleResult.

E.g.

type DistinctResult struct {
	sr *SingleResult
}

func (dr *DistinctResult) Decode(v interface{}) error { return dr.sr.Decode(v) }
func (dr *DistinctResult) Err() error                 { return dr.sr.Err() }

func (dr *DistinctResult) Raw() (bson.RawArray, error) {
	r, err := dr.sr.Raw()
	return bson.RawArray(r), err
}

internal/integration/crud_helpers_test.go Outdated Show resolved Hide resolved
@prestonvasquez
Copy link
Collaborator Author

prestonvasquez commented Apr 26, 2024

@matthewdale would you mind expanding on this concern: #1603 (comment) ? Genericizing SingleResult shouldn't require any user updates for existing methods that return SingleResult. That is, the only user code that would require changes are the usages of collection.Distinct().

@matthewdale
Copy link
Collaborator

@prestonvasquez I added some Sourcegraph search results for code that references mongo.SingleResult and would be impacted by the change to a comment here.

matthewdale
matthewdale previously approved these changes May 2, 2024
Copy link
Collaborator

@matthewdale matthewdale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! 👍

qingyang-hu
qingyang-hu previously approved these changes May 2, 2024
Copy link
Collaborator

@qingyang-hu qingyang-hu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@prestonvasquez prestonvasquez merged commit c481fdf into mongodb:master May 2, 2024
26 of 33 checks passed
@prestonvasquez prestonvasquez deleted the GODRIVER-2443-raw-array branch May 2, 2024 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority-3-low Low Priority PR for Review
Projects
None yet
3 participants