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

chore: store database code as code [DET-9180] #9302

Merged
merged 9 commits into from
May 15, 2024
Merged

Conversation

NicholasBlaskey
Copy link
Contributor

@NicholasBlaskey NicholasBlaskey commented May 3, 2024

Ticket

Description

Store database code as code. See the added readme for details.

Test Plan

CI passes

NOTE TO REVIEWERS

To review the code, we can test that the code is the same schema with pg_dump

on main on a fresh database

pg_dump -U postgres -h 127.0.0.1 --schema-only determined > ../main.sql 

on this branch on a fresh database

pg_dump -U postgres -h 127.0.0.1 --schema-only determined > ../change.sql

check the diff (or go to this link)
https://www.diffchecker.com/8X1MK9aw/

Checklist

  • Changes have been manually QA'd
  • User-facing API changes need the "User-facing API Change" label.
  • Release notes should be added as a separate file under docs/release-notes/.
    See Release Note for details.
  • Licenses should be included for new code which was copied and/or modified from any external code.

@cla-bot cla-bot bot added the cla-signed label May 3, 2024
Copy link

netlify bot commented May 3, 2024

Deploy Preview for determined-ui canceled.

Name Link
🔨 Latest commit b075098
🔍 Latest deploy log https://app.netlify.com/sites/determined-ui/deploys/66450e495706ea0008d9dd21

Copy link

codecov bot commented May 3, 2024

Codecov Report

Attention: Patch coverage is 72.81553% with 28 lines in your changes are missing coverage. Please review.

Project coverage is 51.64%. Comparing base (93c8d81) to head (b075098).
Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9302      +/-   ##
==========================================
+ Coverage   45.29%   51.64%   +6.34%     
==========================================
  Files        1227      839     -388     
  Lines      154095    90398   -63697     
  Branches     2404        0    -2404     
==========================================
- Hits        69805    46685   -23120     
+ Misses      84098    43713   -40385     
+ Partials      192        0     -192     
Flag Coverage Δ
backend 41.81% <72.81%> (+<0.01%) ⬆️
harness 64.09% <ø> (ø)
web ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
master/internal/config/config.go 70.65% <100.00%> (+0.10%) ⬆️
master/cmd/determined-master/migrate.go 16.00% <0.00%> (ø)
master/internal/db/setup.go 47.05% <0.00%> (ø)
master/internal/db/postgres.go 54.25% <82.35%> (+3.37%) ⬆️
master/internal/db/postgres_test_utils.go 82.15% <59.09%> (-1.18%) ⬇️
master/internal/db/migrations.go 66.22% <77.04%> (+6.65%) ⬆️

... and 393 files with indirect coverage changes

@NicholasBlaskey NicholasBlaskey changed the title chore: store database code as code chore: store database code as code [DET-7021] May 3, 2024
@NicholasBlaskey
Copy link
Contributor Author

@jgongd tagged you wanted get thoughts if this design works for streaming updates since yall have a ton of database code

@jgongd
Copy link
Contributor

jgongd commented May 3, 2024

This is a nice way to update views, triggers and functions. Streaming updates is working as expected. cc: @gt2345 @corban-beaird

@NicholasBlaskey NicholasBlaskey marked this pull request as ready for review May 3, 2024 20:00
@NicholasBlaskey NicholasBlaskey requested review from a team as code owners May 3, 2024 20:00
}

// MustResolveTestPostgres is the same as ResolveTestPostgres but with panics on errors.
func MustResolveTestPostgres(t *testing.T) *PgDB {
pgDB, err := ResolveTestPostgres()
func MustResolveTestPostgres(t *testing.T) (*PgDB, func()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this an emerging style in go land to return an object + closing/cancellation function? I thought something like

db := MustResolveTestPostgres(...)
defer db.Close()

would be more idiomatic

Copy link
Contributor Author

@NicholasBlaskey NicholasBlaskey May 6, 2024

Choose a reason for hiding this comment

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

Yeah I have seen it in some places. I know Ryan used it in func ResolveNewPostgresDatabase() (*PgDB, func(), error) {. I think I have seen it in other established Go APIs but I'm not recalling where. The one advantage I see is it more intentional that you need to close something. Its hard for someone to know if they need to or don't need to call the Close().

Doing that, the linter complains that we aren't checking the error. We could add another helper or something. I wanna do another pass on this code as a follow on later to reduce duplication and split tests into isolated db tests soon (so each package would be running in a seperate database instance).

internal/db/postgres_rbac_intg_test.go:354:18: Error return value of `pgDB.Close` is not checked (errcheck)

// ResolveTestPostgres resolves a connection to a postgres database. To debug tests that use this
// (or otherwise run the tests outside of the Makefile), make sure to set
// DET_INTEGRATION_POSTGRES_URL.
func ResolveTestPostgres() (*PgDB, error) {
func ResolveTestPostgres() (*PgDB, func(), error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we didn't close these tests meaning even before this change we have a ton of connections that get left over the course of a test run

I think adding the extra few queries to migrate the test pushed this over the edge. The db handling is kinda of a mess right now. We probably should move away from TestMain everywhere and add per package db isolation.

}

// MustResolveTestPostgres is the same as ResolveTestPostgres but with panics on errors.
func MustResolveTestPostgres(t *testing.T) *PgDB {
pgDB, err := ResolveTestPostgres()
func MustResolveTestPostgres(t *testing.T) (*PgDB, func()) {
Copy link
Contributor Author

@NicholasBlaskey NicholasBlaskey May 6, 2024

Choose a reason for hiding this comment

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

Yeah I have seen it in some places. I know Ryan used it in func ResolveNewPostgresDatabase() (*PgDB, func(), error) {. I think I have seen it in other established Go APIs but I'm not recalling where. The one advantage I see is it more intentional that you need to close something. Its hard for someone to know if they need to or don't need to call the Close().

Doing that, the linter complains that we aren't checking the error. We could add another helper or something. I wanna do another pass on this code as a follow on later to reduce duplication and split tests into isolated db tests soon (so each package would be running in a seperate database instance).

internal/db/postgres_rbac_intg_test.go:354:18: Error return value of `pgDB.Close` is not checked (errcheck)

@NicholasBlaskey NicholasBlaskey requested a review from ioga May 6, 2024 17:32
@NicholasBlaskey NicholasBlaskey changed the title chore: store database code as code [DET-7021] chore: store database code as code [DET-9180] May 6, 2024
@@ -46,7 +46,7 @@ func runMigrate(cmd *cobra.Command, args []string) error {
}
}()

if _, err = database.Migrate(config.DB.Migrations, args); err != nil {
if _, err = database.Migrate(config.DB.Migrations, config.DB.DatabaseCode, args); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Don't mean to bikeshed, but I'd love a more specific term than "database code". I hear database code and I picture all the files containing queries, migrations, etc. Took me a minute to remember what this ticket referred to. Can't think of a great suggestion myself. In my head these are the "stateless" entities but that's probably not the right term.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe "ViewsAndTriggers", just to make the intended contents very clear. Calling it "stateless" or "transient" also suggests that you should be putting anything in there that can't get deleted and recreated routinely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed to views_and_triggers, its not 100% accurate but it probably is more clear the intention of it

Copy link
Member

@mackrorysd mackrorysd left a comment

Choose a reason for hiding this comment

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

I have a couple of suggestions / comments / questions, but I wouldn't block merging once you've considered them.

}

// Update our hash and return we need to create db code.
if _, err := db.sql.Exec("UPDATE db_code_hash SET hash = $1", ourHash); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Reapplying DB code if the hash has changed makes sense, especially to ensure during development it's current. My first thought was that something human-readable here in another column would be great for troubleshooting. It should always match the current running master process anyway, so maybe that's not valuable.. Just thinking out loud.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like overall kill to store something like all the view definitions in the column. Any other ideas?


// Update our hash and return we need to create db code.
if _, err := db.sql.Exec("UPDATE db_code_hash SET hash = $1", ourHash); err != nil {
return nil, false, fmt.Errorf("updating our database hash: %w", err)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we be returning true here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no just returning the zero value.

If an error is not nil the other return values should not be looked at. it is really hard to work with code that returns non nil errors and 'meaningful' values.

return nil
}

var testOnlyDBLock func(sql *sqlx.DB) (unlock func())
Copy link
Member

Choose a reason for hiding this comment

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

This took me a minute to figure out - a comment indicating it gets initialized and acquired in init() in a test module would be a clarity improvement IMO.

Copy link
Member

Choose a reason for hiding this comment

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

I like the way it was factored out, though - much cleaner

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added comment

@@ -396,8 +399,8 @@ var (
}
testGroups = []model.Group{testGroup, testGroupStatic}
testUser = model.User{
ID: 1217651234,
Username: fmt.Sprintf("IntegrationTest%d", 1217651234),
ID: 1217651235,
Copy link
Member

Choose a reason for hiding this comment

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

Just curious - why is this incrementing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was the same value as tests in rbac tests. technically these tests run concurrently as the other test so there is a race condition where one test is in progress running and other test tries to create the user but fails due to duplicate user.

i think this was intended to be globally unique but is a copypasta error.

- The Postgres schema ```determined_code``` will be dropped if it exists.
- Migrations run as they did before ```determined_code``` was added.
- The Postgres schema ```determined_code``` will be recreated.
- All SQL files in the ``db_code`` will be run in lexicographically order.
Copy link
Member

Choose a reason for hiding this comment

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

-ally


Migrations can't see or use views because migrations happen before ```determined_code``` is created.

In the unlikely event this is an issue, you can track views in regular migrations.
Copy link
Member

Choose a reason for hiding this comment

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

Seems like the most likely scenario for an interaction would be a view or trigger referencing a table in the same commit that the table gets created in. For that reason, should we drop code, run migrations, and then recreate the code?

Copy link
Contributor 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 the order it will run in?

On everytime the Determined master starts up

  • The Postgres schema determined_code will be dropped if it exists.
  • Migrations run as they did before determined_code was added.
  • The Postgres schema determined_code will be recreated.
  • All SQL files in the db_code will be run in lexicographical order.

Copy link
Contributor

@eecsliu eecsliu left a comment

Choose a reason for hiding this comment

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

Just one non blocking clarification. I really like the change!

}

func (db *PgDB) dropDBCode() error {
// SET search_path since the ALTER DATABASE ... SET SEARCH_PATH won't apply to this connection
Copy link
Contributor

Choose a reason for hiding this comment

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

im confused by this comment - can you elaborate?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@NicholasBlaskey NicholasBlaskey enabled auto-merge (squash) May 15, 2024 19:37
@NicholasBlaskey NicholasBlaskey merged commit 653a0de into main May 15, 2024
80 of 97 checks passed
@NicholasBlaskey NicholasBlaskey deleted the db_code_2 branch May 15, 2024 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants