Skip to content

GODRIVER-3605 Refactor StringN #2128

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

qingyang-hu
Copy link
Collaborator

@qingyang-hu qingyang-hu commented Jul 15, 2025

GODRIVER-3605
GODRIVER-3561

Summary

Refactor StringN

Background & Motivation

Originally, Element.StringN() does not truncate strings as expected.
This PR also adds a return flag to StringN() to indicate whether the string has been truncated.

@mongodb-drivers-pr-bot mongodb-drivers-pr-bot bot added the priority-3-low Low Priority PR for Review label Jul 15, 2025
Copy link
Contributor

mongodb-drivers-pr-bot bot commented Jul 15, 2025

API Change Report

./v2/x/bsonx/bsoncore

incompatible changes

Array.StringN: changed from func(int) string to func(int) (string, bool)
Document.StringN: changed from func(int) string to func(int) (string, bool)
Element.StringN: changed from func(int) string to func(int) (string, bool)
Value.StringN: changed from func(int) string to func(int) (string, bool)


// If the last byte is not a closing bracket, then the document was truncated
if len(str) > 0 && str[len(str)-1] != '}' {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This line causes GODRIVER-3561

@qingyang-hu qingyang-hu force-pushed the godriver3605 branch 3 times, most recently from acac4b7 to 620794a Compare July 23, 2025 22:48
@qingyang-hu qingyang-hu marked this pull request as ready for review July 23, 2025 23:18
@qingyang-hu qingyang-hu requested a review from a team as a code owner July 23, 2025 23:18
Copy link
Member

@prestonvasquez prestonvasquez left a comment

Choose a reason for hiding this comment

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

Excellent work, Qingyang. Thank you for taking this on 👍


var buf strings.Builder
// stringN stringify a document. If N is larger than 0, it will truncate the string to N bytes.
Copy link
Member

Choose a reason for hiding this comment

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

Can we clarify in the method documentation that the second parameter indicates if the stringified document was truncated?

func (d Document) StringN(n int) string {
if len(d) < 5 || n <= 0 {
return ""
func (d Document) StringN(n int) (string, bool) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we clarify in the method documentation that the second parameter indicates if the stringified document was truncated?

Comment on lines +270 to +276
if n <= 0 {
if l, _, ok := ReadLength(d); !ok || l < 5 {
return "", false
}
return "", true
}
return d.stringN(n)
Copy link
Member

Choose a reason for hiding this comment

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

It's interesting that calling StringN(0) will return an empty string but stringN(0) will return the entire document. The asymmetry makes the code fairly difficult to read. Should we update StringN so that an N <= 0 argument returns the full document?

 func (d Document) String(n int) (string, bool) {
	if n <= 0 {
		return d.stringN(0)
	}

	return d.stringN(n)
}

Ultimately, it's unclear to me why one would call StringN(0) expecting an empty string.

Copy link
Collaborator Author

@qingyang-hu qingyang-hu Jul 24, 2025

Choose a reason for hiding this comment

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

By using stringN(), we can keep the original behavior of StringN(n), which is to return an empty string when n <= 0, while also allowing it to be reused by String(). However, I will be happy to merge stringN() and StringN() if we all accept returning the entire document on StringN(0).

Copy link
Member

Choose a reason for hiding this comment

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

I think that makes sense. Will let @matthewdale opine.

if !ok || length < 5 {
return "", false
}
length -= (4 /* length bytes */ + 1 /* final null byte */)
Copy link
Member

Choose a reason for hiding this comment

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

[nit] Can we re-write this to use // ?

length := (4 + // length bytes
	1) // final null bytes

truncatedStr := bsoncoreutil.Truncate(str, n-buf.Len())
buf.WriteString(truncatedStr)

for length > 0 && !truncated {
Copy link
Member

Choose a reason for hiding this comment

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

[nit] The loop logic is hard to follow, suggest adding comments. I think the following are sufficient:

  1. determine remaining budget l := n - buf.Len()
  2. If buf.Len() >= n, then mark truncated and break
  3. if !first, then write comma, then decrement l and possibly break
  4. ReadElement, subtract its full length, exit on !ok
  5. Delegate to elem.stringN(l), write its str, record its truncated flag

l = n - buf.Len()
}
if !first {
buf.WriteByte(',')
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure we should be writing the comma here? Without testing directly it seems unintuitive. What if n=1 or something and there isn't enough room for a comma?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Another issue is that we don't need a comma if the document only contains one element, even if there is enough space.

for _, tc := range testCases {
for n := -1; n <= len(tc.want)+1; n++ {
t.Run(fmt.Sprintf("StringN %s n==%d", tc.description, n), func(t *testing.T) {
got, _ := tc.doc.StringN(n)
Copy link
Member

Choose a reason for hiding this comment

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

We should validate the expected values for the the truncation bool.

func (e Element) StringN(n int) string {
func (e Element) StringN(n int) (string, bool) {
if n <= 0 {
return "", len(e) > 0
Copy link
Member

Choose a reason for hiding this comment

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

Consider treating n<=0 as "no limit", see comment on document.StringN().

if n <= 0 {
return ""
return "", len(str) > 0
Copy link
Member

Choose a reason for hiding this comment

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

Consider treating n<=0 as "no limit", see comment on document.StringN().

if lens, _, _ := ReadLength(a); lens < 5 || n <= 0 {
return ""
func (a Array) StringN(n int) (string, bool) {
if n <= 0 {
Copy link
Member

Choose a reason for hiding this comment

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

Consider treating n<=0 as "no limit", see comment on document.StringN().

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
Development

Successfully merging this pull request may close these issues.

2 participants