Skip to content

Conversation

jpflueger
Copy link

I noticed that there were no bindings for outbound Postgres in the go sdk. I was able to generate bindings using wit-bindgen 0.3.0 by wrapping outbound-pg.wit into a temporary wit file using an interface for the rdbms-types.wit and a world for the query and execute functions. Most of the binding code was generated but I did make some modifications for better error handling and split out the functions for readability. I wanted to submit these changes as a starting point. Happy to rework whatever you suggest as this is my first time generating bindings and I'm not super familiar with C or Go.

@jpflueger jpflueger requested review from dicej and adamreese March 3, 2023 16:58
@jpflueger jpflueger force-pushed the feature/go-outbound-pg branch from 5465a9c to 16b616b Compare March 3, 2023 17:01
@jpflueger
Copy link
Author

Hmm. I must have done something funky to get those merge conflicts. I'll try to resolve and re-push my changes.

Justin Pflueger added 7 commits March 3, 2023 10:13
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
Signed-off-by: Justin Pflueger <justin.pflueger@fermyon.com>
@jpflueger jpflueger force-pushed the feature/go-outbound-pg branch from 16b616b to 7088ec0 Compare March 3, 2023 17:20
@jpflueger
Copy link
Author

Hmm. I must have done something funky to get those merge conflicts. I'll try to resolve and re-push my changes.

Weirdness caused by me not signing commits so I had to rebase with a merge conflict. Fixed locally and force-pushed my branch to update, sorry for the churn.

Copy link
Contributor

@dicej dicej left a comment

Choose a reason for hiding this comment

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

Looks great overall -- just a couple of requests.

GENERATED_OUTBOUND_REDIS = redis/outbound-redis.c redis/outbound-redis.h
GENERATED_SPIN_REDIS = redis/spin-redis.c redis/spin-redis.h
GENERATED_KEY_VALUE = key_value/key-value.c key_value/key-value.h
GENERATED_OUTBOUND_PG = postgres/outbound-pg.c postgres/outbound-pg.h
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you please add postgres/sdk-version-go.c to SDK_VERSION_DEST_FILES below? That helps ensure the SDK version information is embedded in components built with the SDK.

Copy link
Author

Choose a reason for hiding this comment

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

Will do, I need to rebase onto v1.0 branch first and then I'll add it to avoid any merge conflicts.

@@ -0,0 +1,18 @@
# Spin component with outbound postgres in TinyGo
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind adding instructions for using wit-bindgen 0.3.0 to re-generate the bindings if necessary (e.g. because we add variant cases, new functions, etc. to outbound-pg.wit)?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I'll need to change a decent amount of the code. I broke things out into different files in order to better understand how things were working but that would be tedious to someone who is just trying to update the bindings quickly.

Copy link
Author

Choose a reason for hiding this comment

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

@dicej I added instructions to the readme in the sdk/go folder. Let me know if that is too hacky to include :)

Copy link
Collaborator

@itowlson itowlson left a comment

Choose a reason for hiding this comment

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

I'm a Go ignoramus so please ignore me if I'm completely off beam


func (n DbValue) GetBoolean() bool {
if g, w := n.Kind(), DbValueKindBoolean; g != w {
panic(fmt.Sprintf("Attr kind is %v, not %v", g.String(), w.String()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know Go database idioms but panicking on the wrong type seems unforgiving. Would it make sense to have a Get and TryGet function for each type, e.g. GetBoolean() bool and TryGetBoolean() bool, bool for the if val, ok := v.TryGetBoolean(); ok { ... } idiom?

Are there existing Go database API patterns we could try to emulate, or is our WIT just too far away from them?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is just how the tinygo binding generator in wit-bindgen works. The idea is that the correct way to get the value is ask what the type is first and then extract the value using the appropriate getter. It's like poor-man's pattern matching. So if the app developer skips the first step and just assumes the type, it's a programmer error, hence the panic. By that reasoning, making the developer check for errors in the second step would be redundant.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed with what Joel said here. With the lack of generics in Go usually this method is accompanied by a check like this:

if dbValue.Kind() == DbValueKindBoolean {
  dbBool := dbValue.GetBoolean()
}

My understand of the go idioms is that a tuple is essentially used like a Result<T> in rust where a function like func x() (bool, Error) is equivalent to fn x() -> Result<bool, Error> in the Rust world.

All that being said, this is generated code so our request will need to go to wit-bindgen.

n.kind = DbValueKindBinary
}

func DbValueDbNull() DbValue {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we have something like an IsNull function? What sort of experience should we expect for loading a value from a NULLable column, especially if the developer wants graceful failure rather than panic?

(Again this is me being ignorant of How Things Are in Go.)

Copy link
Author

Choose a reason for hiding this comment

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

Technically in Go, null == nil so our language is a little off but we could add a function like this in an additional layer of our SDK. I did find that check necessary, especially on nullable columns where the column type is actually string but there was no value so the kind in our sdk was DbValueKindNull instead of string with a nil value.

func (v *DbValue) IsNull() bool {
  return v.Kind() == DbValueKindBoolean
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let me clarify what I'm driving at. Consider a People table with two (interesting) columns, Age (integer) and MiddleName (string). Both are NULLable in the database (to reflect unknown age and people who don't have a middle name).

I want to query for the set of People and process that list, for example rendering to HTML (printing unknown for NULL ages and - for no middle name), or marshalling to JSON (where I guess I'd omit the NULL fields), but for generality going via some intermediate Go structure.

How (generally) do I do this?

If memory serves, for the intermediate structure to allow nil, its values would have to be pointers e.g. { Age *int, MiddleName *string }. Is that right? If so, what is the outline for populating them, something like:

var age
if v.Kind() == DbNull {
  age = nil
} else {
  age = &v.GetInt()
}

person := Person { Age: age, ... }

Is that right (modulo Go syntax!), and if so, does it seem like what a Go programmer would expect to right?

To be clear, I am not suggesting it's not! I'm not familiar with Go idioms - for all I know this is exactly what a Go programmer would find familiar. I'm just confirming that we are happy with whatever the pattern for nullable columns is.

Hope this makes more sense now - sorry for the somewhat muddled question first time around!

Copy link
Author

Choose a reason for hiding this comment

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

If memory serves, for the intermediate structure to allow nil, its values would have to be pointers e.g. { Age *int, MiddleName *string }. Is that right?

I do believe that is right, yes. When I created a struct without pointers, the struct is initialized with default values (i.e. 0 for int, "" for string, etc.)


func (n PgError) GetConnectionFailed() string {
if g, w := n.Kind(), PgErrorKindConnectionFailed; g != w {
panic(fmt.Sprintf("Attr kind is %v, not %v", g.String(), w.String()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again panics seem quite harsh, especially given that the developer can't expect errors the way they can data types... is the expectation that the app would call Kind() before calling the relevant Get function?

Copy link
Author

Choose a reason for hiding this comment

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

At least the way that I've used it and the way the generated code works, I'm always forced to check the kind before calling this function.

return n.val.(string)
}

func (n *PgError) SetConnectionFailed(v string) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not quite sure where an app would use these functions - could they be private?

Copy link
Author

Choose a reason for hiding this comment

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

The set functions could be private yes but this is just how wit-bindgen works as it stands today. I'm unclear on how we could customize this behavior. We could wrap the generated code better but that would incur a maintenance cost.

@Mossaka
Copy link
Contributor

Mossaka commented Mar 7, 2023

Hey I am glad that you were able to use the wit-bindgen-go generator to generate the bindings! I would greatly appreicate any feedback to improve the binding's ergonomics as I didn't pay too much attention to it as I was trying to get the it done as soon as possible.

@radu-matei
Copy link
Member

Hey, @Mossaka — I think this is still using the C bindings generator.

@jpflueger
Copy link
Author

@radu-matei actually this is using both. I used our pinned version of wit-bindgen to generate the C bindings. Then I took our wit files and added a default world so it was compatible with the newer wit-bindgen version, used the wit-bindgen-go generator and copied the go files over. I did this as an experiment so I didn't have to write the bindings myself so this is definitely a hack.

@Mossaka it was pretty easy to use once I got our WIT into the right format. I do have some small feedback for the generated code and I'll file issues in the wit-bindgen repo so you can keep track of them there.

Justin Pflueger added 3 commits March 13, 2023 16:05
@vdice
Copy link
Contributor

vdice commented Nov 9, 2023

@jpflueger Outbound Postgres for the Go SDK landed in #1922. Shall we close this?

@jpflueger jpflueger closed this Nov 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants