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

Why the usage of int to hold values that overflow it (when it can only 32 bits wide) #24

Closed
bbuck opened this issue Oct 15, 2015 · 11 comments

Comments

@bbuck
Copy link
Collaborator

bbuck commented Oct 15, 2015

In the scalars.go file the coerceInt function and intOrNil deal with checking against large integer values but they use the int type:

var (
    MaxInt = 9007199254740991
    MinInt = -9007199254740991
)

func coerceInt(value interface{}) interface{} {
    switch value := value.(type) {
    case bool:
        if value == true {
            return int(1)
        }
        return int(0)
    case int:
        return value
    case float32:
        return intOrNil(int(value))
    case float64:
        return intOrNil(int(value))
    case string:
        val, err := strconv.ParseFloat(value, 0)
        if err != nil {
            return nil
        }
        return coerceInt(val)
    }
    return int(0)
}

// Integers are only safe when between -(2^53 - 1) and 2^53 - 1 due to being
// encoded in JavaScript and represented in JSON as double-precision floating
// point numbers, as specified by IEEE 754.
func intOrNil(value int) interface{} {
    if value <= MaxInt && value >= MinInt {
        return value
    }
    return nil
}

The constants you have for MaxInt and MinInt will only work fine when this is running on a 64-bit machine. I think it would be much safer to use int64 explicitly here since that appears to be what you really want and you can eliminate the 32 vs. 64 issues completely. This isn't something I've directly run across, but it just feels wrong to depend on the variable size int type when you're using values that fit only inside of an int64.

@bbuck bbuck changed the title Why the usage of int to hold values that overflow it (when it's only 32 bits wide) Why the usage of int to hold values that overflow it (when it can only 32 bits wide) Oct 16, 2015
@sogko
Copy link
Member

sogko commented Oct 16, 2015

Hi @bbuck

Thanks for sharing your time to take a look into this project, appreciate it a lot! 👍🏻

I contributed part of that chunk of code that was ported from graphql-js. At that point of time, I found it strange that integer value was restricted that way and wondered whether we should just support int64 since golang does not the same limitations as Javascript.

But looking deeper into it, I found out that the GraphQL specs specify support only for signed 32-bit integers. But to add to the confusion, it seems that graphql-js implementation supports 52-bits integer.

There have been discussions in the graphql-js camp about 64-bit support and the direction for graphql-js whether to update the spec or implementation regarding this 52-bits inconsistency.

At this moment, it seems that a part of the GraphQL community is voting for a strict specs of signed 32-bits integer on all platforms, both client and server. (I encourage everyone else to make your voice be heard as well).

Personally, I would love to adopt support for 64-bit since golang provides it for free (yay!) but we also have to consider GraphQL clients that consume the results won't necessarily support 64-bit. So it makes sense to support the lowest common denominator.

What do you guys think?

/cc @chris-ramon

@bbuck
Copy link
Collaborator Author

bbuck commented Oct 16, 2015

I guess what I was more getting it is that internally I feel that int64 should be used, but externally (as per the spec) 32 bit integers (or the 52 bit values as you mention) could be returned. Basically what I did modifying the coerceInt (and the corresponding one for float) function in my pull request.

Mostly because the application itself might not use int, like in my use case where gorm defaults it's ID field to uint. It seems logical to just return a uint and let the GraphQL system convert that uint to whatever value best relates to it's GraphQLInt type.

The best comparison I have is that, during this session of this one particular course here (which may or may not be accessible to you, I hope so) they discuss returning a JavaScript object when graphql-js was told to expect a GraphQLString so the final result returned is "[object Object]" (the closest to a string for a standard object GraphQL could get).

tl;dr Internally I think graphql-go should handle "maximum" values (so to speak) and just convert them to the final result that GraphQL has standardized.

Also, you're welcome. I've just started (like less than a week ago) diving into GraphQL and I'm super excited about it and want something to do in Go so I hope to try and start contributing some useful things going forward.

@sogko
Copy link
Member

sogko commented Oct 16, 2015

Ah, I see what you mean right now lol 👍🏻

Yeah you are right, those MaxInt and MinInt would be a problem on 32-bit platforms, (shown here on a 32-bit golang playground: http://play.golang.org/p/BZlMvi86aW)

prog.go:9: constant 9007199254740991 overflows int
prog.go:10: constant -9007199254740991 overflows int

Specifying them as int64 as you've suggested would address that issue: http://play.golang.org/p/BtL6YFAFeg

amd64p32 nacl go1.5.1
MaxInt 9007199254740991
MinInt -9007199254740991
int(MaxInt) -1
int(MinInt) 1

Thanks for that catch 😉

I see you already have a PR open; you think you want to include these changes in them as well?

And I believe those MaxInt and MinInt could be specified as const as well, what do you think?

Again, appreciate your contribution and looking forward for more!

@bbuck
Copy link
Collaborator Author

bbuck commented Oct 16, 2015

I think both great points. I'll throw that in the PR!
On Thu, Oct 15, 2015 at 20:32 Hafiz Ismail notifications@github.com wrote:

Ah, I see what you mean right now lol 👍🏻

Yeah you are right, those MaxInt and MinInt would be a problem on 32-bit
platforms, (shown here on a 32-bit golang playground:
http://play.golang.org/p/BZlMvi86aW)

prog.go:9: constant 9007199254740991 overflows int
prog.go:10: constant -9007199254740991 overflows int

Specifying them as int64 as you've suggested would address that issue:
http://play.golang.org/p/BtL6YFAFeg

amd64p32 nacl go1.5.1
MaxInt 9007199254740991
MinInt -9007199254740991
int(MaxInt) -1
int(MinInt) 1

Thanks for that catch 😉

I see you already have a PR open; you think you want to include these
changes in them as well?

And I believe those MaxInt and MinInt could be specified as const as
well, what do you think?

Again, appreciate your contribution and looking forward for more!


Reply to this email directly or view it on GitHub
https://github.com/chris-ramon/graphql-go/issues/24#issuecomment-148570851
.

@bbuck
Copy link
Collaborator Author

bbuck commented Oct 16, 2015

@sogko So I guess I have a dilemma here. Should we be returning int32 values? If so we can't handle the same numeric ranges. Yet if we use 64 bit we can constrain it ourselves but we violate the spec. What do you think?

@sogko
Copy link
Member

sogko commented Oct 16, 2015

Hey @bbuck

I think for integer values (int, int32, int64, uint, et al) should return the same type as its input when passed into coerceInt().

intOrNil() then exists to check if integer value falls within allowed range (currently signed 52-bit range, might change if graphql-js updates its implementation)

This is what I imagine the implementation it could be.

// intOrNil checks if value is within allowed range. 
// Returns the original value if ok, else nil.
func intOrNil(value interface{}) interface{} {
    val := int64(value)
    if val <= MaxInt && val >= MinInt {
        return value
    }
    return nil
}

// For non-integers (strings, bool, floats), coerce to `int`
// For integers (including its variations), value should be within 
// signed 52-bit range for its original input type. Else returns nil
// TODO: The below code can be written more elegantly I guess lol
func coerceInt(value interface{}) interface{} {
    switch value := value.(type) {
    case bool:
        if value == true {
            return int(1)
        }
        return int(0)
    case int:
        return intOrNil(value) // returns as int or nil
    case int32:
        return intOrNil(value) // returns as int32 or nil
    case int64:
        return intOrNil(value) // returns as int64 or nil
    case uint:
        // TODO: check if uint value is larger than signed int64
        return intOrNil(value) // returns as uint or nil
    // TODO: case uint8, uint16 etc..
    case float32:
        if intOrNil(value) == nil {
          return nil
        }
        return int(value) // casts float32 as int
    case float64:
        return intOrNil(value) // returns as int64 or nil
    case string:
        val, err := strconv.ParseFloat(value, 0)
        if err != nil {
            return nil
        }
        return coerceInt(val)
    }
    return int(0)
}

What do you think about this?

@bbuck
Copy link
Collaborator Author

bbuck commented Oct 16, 2015

Okay I've updated my pull request with some of the ideas you posted. I modified the switch piece so that it returns the "best choice" of int* type based on it's input type. So long as the value sits within [MinInt, MaxInt] and is an integer then that integer type is returned (i.e. int32(1) and input returns int32(1) as output). For uint* types they're cast to the size up int* value (if they're less than MaxInt) so uint8 ([0, 255]) becomes an int16. Any value (uint/uint64, int64, float32, float64) that can deviate from the range [MinInt, MaxInt] are passed through intOrNil if they are small enough to fit within an int64 value (which intOrNil now takes as input).

I've updated the tests so that corresponding types are validated.

Personally, I'm not sure how comfortable I feel with that varied return values. I think I would be more comfortable if only int64 or nil was returned. But that could just be me.

@bbuck
Copy link
Collaborator Author

bbuck commented Oct 16, 2015

After pushing and commenting I suppose there is little reason specifically with forcing uint* to int* at this point as long they're within the min/max range.

@sogko
Copy link
Member

sogko commented Nov 6, 2015

Just to keep this issue up-to-date:

I think this issue is still valid, and now that most of the codebase has settle down a little bit from the API changes, we can re-look into this.

@bbuck
Copy link
Collaborator Author

bbuck commented Dec 3, 2015

Just to not here as well that #83 is an updated proposed fix. Also fixes the invalid bit size (according to spec) numbers used which was also updated in the graphql-js library.

@bbuck
Copy link
Collaborator Author

bbuck commented Jan 8, 2016

This should be closed now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants