Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

feat/enterpriseportal: database layer for subscriptions upsert#63703

Merged
bobheadxi merged 4 commits into
mainfrom
07-08-feat_enterpriseportal_database_layer_for_subscriptions_upsert
Jul 9, 2024
Merged

feat/enterpriseportal: database layer for subscriptions upsert#63703
bobheadxi merged 4 commits into
mainfrom
07-08-feat_enterpriseportal_database_layer_for_subscriptions_upsert

Conversation

@bobheadxi

@bobheadxi bobheadxi commented Jul 9, 2024

Copy link
Copy Markdown
Member

Implements upsert for all the subscriptions fields in the DB client. As part of this I generalized the logic for building upsert DB interactions into a new upsert package, because this pattern is a common one we'll need to implement to maintain various AIP-update-compliant endpoints, which specifies various upsert behaviours: https://google.aip.dev/134

Part of CORE-216
Part of CORE-156

Test plan

Integration tests against DB

@cla-bot cla-bot Bot added the cla-signed label Jul 9, 2024

bobheadxi commented Jul 9, 2024

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Join @bobheadxi and the rest of your teammates on Graphite Graphite

@bobheadxi bobheadxi marked this pull request as ready for review July 9, 2024 01:16
@bobheadxi bobheadxi requested review from a team and eseliger July 9, 2024 01:16

@eseliger eseliger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nothing is blocking, just a couple of questions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thanks for this comment, these one-liners are always worth so much when trying to understand what's going on

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

crazy definition language 😬

Comment on lines 86 to 88

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might be a good opportunity to create a helper for this that auto does this while scanning

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

How would I do that? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe

type UTCTime struct{ *time.Time }

// Scan implements the Scanner interface.
func (nt *NullTime) Scan(value any) error {
	*nt.Time, _ = value.(time.Time).UTC()
	return nil
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Took a bit of finesse, but I think this is how you do it:

package utctime

import (
	"database/sql"
	"database/sql/driver"
	"time"

	"github.com/sourcegraph/sourcegraph/lib/errors"
	"github.com/sourcegraph/sourcegraph/lib/pointers"
)

// Time is a wrapper around time.Time that implements the database/sql.Scanner
// and database/sql/driver.Valuer interfaces to serialize and deserialize time
// in UTC time zone.
type Time time.Time

// Now returns the current time in UTC.
func Now() Time { return Time(time.Now().UTC()) }

var _ sql.Scanner = (*Time)(nil)

func (t *Time) Scan(src any) error {
	if src == nil {
		return nil
	}
	if v, ok := src.(time.Time); ok {
		*t = Time(v.UTC())
		return nil
	}
	return errors.Newf("value %T is not time.Time", src)
}

var _ driver.Valuer = (*Time)(nil)

// Value must be called with a non-nil Time. driver.Valuer callers will first
// check that the value is non-nil, so this is safe.
func (t Time) Value() (driver.Value, error) {
	stdTime := t.AsTime()
	return *stdTime, nil
}

// AsTime returns the underlying time.Time value, or nil if it is nil.
func (t *Time) AsTime() *time.Time {
	if t == nil {
		return nil
	}
	// Ensure the time is in UTC.
	return pointers.Ptr((*time.Time)(t).UTC())
}

follow-up PR coming to use this for all time :) Thank you!

Comment thread cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go Outdated
Comment thread cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm curious why we're trying to build an ORM here 😬 So far in sg/sg we've mostly been quite explicit with the queries we write, and if we make our own ORM builder anyways, could we get away even cheaper if we just used the active record features of gorm directly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In gorm, a lot of the options are automagical annotations (as you point out in https://github.com/sourcegraph/sourcegraph/pull/63703#discussion_r1669523483 quickly gets crazy), and the feature set is pretty broad. IMO this gives us some more control in a smaller API surface, which lines up with being explicit, without being super repetitive/error-prone to write, as I find some of our queries get. WDYT?

@bobheadxi bobheadxi Jul 9, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In particular this just takes what would be a huge list of if/else statements we would have to write several times for each of our CRUD resources, and allows it to be backed by a dedicated helper, so I don't think it's quite a full-fledged ORM

Comment thread cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go Outdated
@bobheadxi bobheadxi force-pushed the 07-08-feat_enterpriseportal_database_layer_for_subscriptions_upsert branch from bd1e3cf to 7741d00 Compare July 9, 2024 20:47
@bobheadxi bobheadxi merged commit 28f797e into main Jul 9, 2024
@bobheadxi bobheadxi deleted the 07-08-feat_enterpriseportal_database_layer_for_subscriptions_upsert branch July 9, 2024 21:35
Comment on lines +192 to +203
b := upsert.New("enterprise_portal_subscriptions", "id", opts.ForceUpdate)
upsert.Field(b, "id", id)
upsert.Field(b, "instance_domain", opts.InstanceDomain)
upsert.Field(b, "display_name", opts.DisplayName)

upsert.Field(b, "created_at", opts.CreatedAt,
upsert.WithColumnDefault(),
upsert.WithIgnoreOnForceUpdate())
upsert.Field(b, "updated_at", time.Now()) // always updated now
upsert.Field(b, "archived_at", opts.ArchivedAt)
upsert.Field(b, "salesforce_subscription_id", opts.SalesforceSubscriptionID)
upsert.Field(b, "salesforce_opportunity_id", opts.SalesforceOpportunityID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not gonna line - literal SQL queries is far more readable than this when the entire query can be written at once, but I do see when there are lots if-conditions, this pattern/helper helps.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this is important to have because otherwise the AIP upsert pattern is far too annoying to build for many fields. FWIW, using -v in tests will dump the full query for every interaction in the test output

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did read through all (unresolved) comments :D any chance we can make this package to allow chained calls (similar to what GORM provides), e.g.

b := upsert.New(...).Fields(...)
if ... {
	b = b.Fields(...)
}

😂 maybe too fancy tho

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure that makes it nicer as now you have to worry about assignment etc 🤔 Additionally, the design right now uses a bit of generics which b.Fields will not allow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ahh right... Go generics can't be used on methods (yet?), alright nvm 😬

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants