-
Notifications
You must be signed in to change notification settings - Fork 116
[Major Change] Introduce types
package for type handling in sqlgen
#142
Conversation
be49296
to
4d25354
Compare
types
package for type handling in sqlgen
4d25354
to
7d8e175
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overall comments:
- feel like there are package/high level comments needed in the early commits.
- think the write path is not there yet.
types/field.go
Outdated
// Blank creates a blank copy of a field. This _must_ be used in order to create a | ||
// sql.Scanner from a field. | ||
// Example: | ||
// f := FieldFromValue(map[string]string{"sql": "binary", i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add indent so that this shows up as a code block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, i think this example code is out of date
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't be needed any more
types/sql.go
Outdated
if iface, ok := i.(encoding.BinaryMarshaler); ok { | ||
return iface.MarshalBinary() | ||
} | ||
case "string", "text": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason to support both tags? can we support only one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea here is that you might have varchar or a text blob - though I'd be fine with changing it to just string
types/sql.go
Outdated
} | ||
|
||
// Handle sql type overrides as special cases. | ||
switch f.TagType["sql"] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it safe to reuse the sql
struct field tag in this way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed this
types/sql.go
Outdated
// Handle native types. | ||
switch i.(type) { | ||
case *[]byte: | ||
fmt.Println("bytes") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
types/field.go
Outdated
"reflect" | ||
) | ||
|
||
type TagSet map[string]struct{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a comment for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
think all of the exported symbols in this file could use an explanation, and a package-level comment explaining how this is going to be used would be helpful. I have trouble understanding the higher level intent of this code by looking at this commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
sqlgen/reflect.go
Outdated
ptr := reflect.New(table.Type) | ||
elem := ptr.Elem() | ||
|
||
for i, column := range table.Columns { | ||
value, _ := scannables[i].(driver.Valuer).Value() | ||
// These values are all non-pointers and have been copied. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does this imply?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment here
types/field.go
Outdated
@@ -68,5 +68,21 @@ func (f *Field) Blank() *Field { | |||
return field | |||
} | |||
|
|||
// CopyTo copies the reference onto the given value. Note: this uses pointers, so maybe this is a | |||
// bad name? | |||
func (f *Field) CopyTo(val reflect.Value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe SetIn
or SetValue
would help?
sqlgen/reflect.go
Outdated
return nil, err | ||
} | ||
|
||
return BuildStruct(table, scannables), nil | ||
// Coerce fields into their actual type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
almost think we should remove this comment. at first i thought it meant something more than "satisfy scanner.Scan
" was happening
testfixtures/custom_type.go
Outdated
@@ -5,25 +5,28 @@ import ( | |||
"fmt" | |||
) | |||
|
|||
type CustomType []byte | |||
type CustomType [16]byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you explain this fixture change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep: I wanted this to no longer be a natively supported SQL type
types/field.go
Outdated
IsNil bool | ||
} | ||
|
||
func FieldFromValue(in interface{}, tags []string) *Field { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, having read the whole PR now, I think I'd like to see this in _test.go
only - it doesn't seem like there is a reason to use this outside of test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed this entirely
864ff0d
to
1193918
Compare
Discounting tests, comments and test data, this only adds ~200 LOC. 😬 |
49c2702
to
7c32ad3
Compare
Pull Request Test Coverage Report for Build 922
💛 - Coveralls |
fields/sql.go
Outdated
if iface, ok := i.(encoding.BinaryMarshaler); ok { | ||
return iface.MarshalBinary() | ||
} | ||
case f.Tags.Contains("string"), f.Tags.Contains("text"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: think one of your comment responses mentions removing text
. i think having only one here would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
fields/sql.go
Outdated
func (f Valuer) Value() (driver.Value, error) { | ||
// Ideally, all the interfaces below would be implemented as normal methods, not indirect | ||
// methods. However, in order to be safe we take the address of pointers where we can. | ||
// This specifically supports protobuf. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this comment referring to the next line? i am having trouble understanding what this implies for my read here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should move this into the descriptor.Valuer method
// } | ||
switch { | ||
case f.Tags.Contains("binary"): | ||
if iface, ok := i.(marshaler); ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you help me understand the difference between marshaler and binary marshaler? why would one implement one vs. the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've attempted to clarify this by the interface. This specifically supports protobuf and other libraries that implement Marshal() (I know I've seen it before but I can't remember one off the top of my head)
fields/sql.go
Outdated
case f.Tags.Contains("json"): | ||
if b, ok := i.([]byte); ok { | ||
return b, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why would one use the json
tag if the value is a string? i.e. there is nothing for the marshaler to do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can remove this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
fields/interface.go
Outdated
@@ -0,0 +1,9 @@ | |||
package fields | |||
|
|||
type marshaler interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you point to some places in the standard library where this is used but not exported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are similar but not exactly the same patterns in the stdlib. We looked at gogo/protobuf#28 together. I'm still not clear on why it wasn't implemented - the issue was fairly confusing and from over 3 years ago.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
github.com/golang/protobuf/proto.Marshaler and github.com/golang/protobuf/proto.Unmarshaler?
s.value.Set(reflect.ValueOf(src)) | ||
return nil | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you comment on why these need to be handled in this codepath, but not in the valuer path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since these are native types, we don't need to do any special handling. Because scanner returns an error for unhandled types, we need to explicitly handle them (as opposed to how valuer works where SQL will pick up our slack)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be handled before Scanner
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I think that helps.
@@ -6,6 +6,7 @@ import ( | |||
"net/http" | |||
"sort" | |||
|
|||
_ "github.com/go-sql-driver/mysql" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is a lot going on in this last commit. it'd be great to split it up into a few different changes - maybe by valuer/scanner/livesql.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in order to "install" the mysql driver.
996d78c
to
f73398c
Compare
@stephen Hopefully these commits are a little cleaner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think one other thing that would have helped my understanding here is a high level comment about the API surface area, i.e. this work does help in cases where:
- we control the package and can add additional interfaces to a type
- the package itself implements standard text/binary marshalling interfaces
it doesn't help with things like:
- types outside of our controlled packages that don't support marshalling interfaces, e.g. arbitrary conversion for
time.Time
@@ -319,7 +332,7 @@ func makeWhere(table *Table, filter Filter) (*SimpleWhere, error) { | |||
return nil, fmt.Errorf("unknown column %s", name) | |||
} | |||
|
|||
l = append(l, whereElem{column: column, value: value}) | |||
l = append(l, whereElem{column: column, value: column.Descriptor.Valuer(reflect.ValueOf(value))}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to verify - this is a departure from the existing behavior because the filters values are now converted vs. used directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
}) { | ||
t.Error("bad select") | ||
} | ||
assert.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did the tests from here down in this file change due to api changes, or as cleanup? if the latter, it'd be nice to have them separate/noted that way for reviewing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😬 it was admittedly cleanup. I tried to extract these all into a separate PR but some of them got away from me
if iface, ok := i.(encoding.TextMarshaler); ok { | ||
return iface.MarshalText() | ||
} | ||
case f.Tags.Contains("json"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mentioned irl, but i think it would have been nice to see string/json support as a follow up vs. in this initial implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah :|
fields/sql.go
Outdated
type Scanner struct { | ||
*Descriptor | ||
value reflect.Value | ||
IsValid bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does IsValid
need to be persisted on the struct? I see that it is only referenced within Scan
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, since we treat the value as a non-pointer value, IsValid tells us whether or not something is nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you point to where that is used outside of Scan
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point - this should be internal API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(it's used specifically in CopyTo)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, thanks i missed that. making it package private sounds good
s.value.Set(reflect.ValueOf(src)) | ||
return nil | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I think that helps.
} | ||
if !ok { | ||
return nil, fmt.Errorf("bad type %s: field %s has unsupported type %s", typ, field.Name, field.Type) | ||
d := fields.New(field.Type, tags[1:]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it safe for us to reuse the sql
tag in this manner? should we use a different tag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sql tag is explicitly being used by only us in sqlgen. I think we should just use sql
everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, yep, that makes sense. i had it in my head that other software used that tag too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other ORMs do AFAIK, but nothing in the go stdlib (and the driver doesn't either)
77cfe65
to
9ce377a
Compare
Includes #150 Allocations went up:
|
fields/interface.go
Outdated
@@ -0,0 +1,9 @@ | |||
package fields | |||
|
|||
type marshaler interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
github.com/golang/protobuf/proto.Marshaler and github.com/golang/protobuf/proto.Unmarshaler?
// bool | ||
// []byte | ||
// string | ||
// time.Time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@berfarah time.Time is annoying here because mysql timestamp stores 6 digits after decimal point (microseconds), but time.Time stores nanoseconds. This means I can't write a time.Now() and expect to read back the same value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's where we can use Scan
/Value
to convert stuff more effectively
fields/sql.go
Outdated
// time.Time | ||
// nil - for NULL values | ||
func (f Valuer) Value() (driver.Value, error) { | ||
i := f.value.Interface() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we check f.value.IsValid() first?
reflect.ValueOf(interface{}(nil)).Interface()
panics, although I don't see if we can even define a sqlgen model struct with interface{} type field
// If our interface supports driver.Valuer we can immediately short-circuit as this is what the | ||
// MySQL driver would do. | ||
if valuer, ok := i.(driver.Valuer); ok { | ||
return valuer.Value() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we have Valuer handle nil case itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For valuer, the nil case will panic (I checked). In scanner we can and do handle it.
} | ||
|
||
var _ driver.Valuer = Valuer{} | ||
var _ driver.Valuer = &Valuer{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, but it's nice to know that we are fulfilling the interface and the Go stdlib introduces this pattern. It only runs once so allocations are constant time (and presumably garbage collected)
fields/sql.go
Outdated
// time.Time | ||
// nil - for NULL values | ||
func (s *Scanner) Scan(src interface{}) error { | ||
// Get a value of the pointer of the pointer of our type. The Scanner and Unmarshalers should |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why this is returning pointer to pointer? For a field like type User struct { Name string }
, I think it's type.Kind() is reflect.String?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment is actually just wrong. of the pointer
should be there once. Will update.
type Scanner struct { | ||
*Descriptor | ||
value reflect.Value | ||
isValid bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this? Can we make it never write valid reflect.Value if not isValid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isValid
is more following the style of NullBytes
/NullBool
/etc where it means "is not nil"
1a21b03
to
e4aaee1
Compare
The fields package abstracts away dealing with and coercing types for struct fields. A descriptor is responsible for keeping track of all type information associated with a struct field. It automatically dereferences the value and remembers that we're dealing with pointers in order to reduce the permutations of value assignment in dealing with the reflected values. Now rather than having to check both values if they are pointers when assigning (2*2 permutations, 4 code branches) you can simply know that this side is always dereferenced and only assign pointers when
It seems like this is used by a lot of packages but not explicitly referenced in the standard library. Adding it to this package as a native interface for handling it.
Fields can be converted to SQL by creating a Valuer from their descriptor.
SQL values can be serialized into fields by creating a Scanner from their corresponding field's descriptor.
This is a registration-time validation that the type is in fact a valid SQL type.
Ensure we fully support Protobuf by adding a test for it.
This is for more pleasant debugging/diff output
MySQL needs to be imported in order for the driver to be registered (AFAIK it's registered via `init()`)
Gives us better diffing
e4aaee1
to
5f08128
Compare
@stephen since this is a pretty big breaking change, I'm thinking we should tag this with a 1.0 release. What do you think? |
{Out: float32(5), In: float64(5)}, | ||
{Out: likeFloat(5), In: float64(5)}, | ||
// Interfaces without tags: | ||
{Out: ifaceScanner{[]byte("scan me")}, In: []byte("scan me")}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pointer ifaceScanner test case?
Goals
Changes
This PR adds a types package with the intention of replacing Scannables / TypeConverters. This unifies all of our logic in one place for determining how values are converted, while also supporting native interfaces.
Implementation
Empty
/Interface
is being called when scanning values.MustRegisterCustomScalar
andMustRegisterSimpleScalar
.Usage
(but really, after this is implemented for SQL, look at where we could use it on the GraphQL side as well)