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

Database queries with Diesel #192

Merged
merged 54 commits into from
Sep 1, 2021
Merged

Database queries with Diesel #192

merged 54 commits into from
Sep 1, 2021

Conversation

david-crespo
Copy link
Contributor

@david-crespo david-crespo commented Aug 8, 2021

This PR does the minimum required to fetch a project with Diesel. This test surfaced a number of little problems we would have to solve to use Diesel to replace most of our SQL wrangling code.

This PR converts all of our database queries to use Diesel.

It's worth noting that the "unreleased changes" part of the Diesel changelog was referred to somewhere as Diesel 2.0 (can't remember where). Here's the Diesel 2.0 milestone. It has been in the works since 2019, though, so I don't think there's much reason to expect it to come out in the next month or two.

We're using Diesel master because it makes the async stuff much easier. We may be able to help move Diesel 2.0 along.

Things to solve

Note: this was an early to-do list and is therefore not even close to comprehensive.

  • Set up connection pool
  • Basic working schema
  • Query returning one thing
  • Query returning a list
  • Make custom column types like Name work
  • More precise schema (e.g., use things like String(63) instead of Text)
  • Transform diesel::result::Error into our Error, which includes useful info like "by ID" or "by name" on a not found error. This would fix the test failures, which are caused by different kinds of project fetch errors all coming back as 500s.
  • Update query
  • Figure out how to see the SQL Diesel produces
  • Make list query more good
  • Figure out how to write fancier queries like sql_update_precond
  • Make everything async

}
}
}

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 probably won't work out because in order to produce our Error, we need other info from context that DieselError can't give us

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, that might be okay - just means that we can define more featureful converters from DieselError to Error, depending on the type.

I imagine that NotFound triggers more work for the caller to add context, and everything else is more-or-less passthrough.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've gone ahead and updated these with two explicit constructions that provide enough context to pass all tests.

Copy link
Contributor Author

@david-crespo david-crespo Aug 13, 2021

Choose a reason for hiding this comment

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

This is great! Exactly what I was picturing, but I didn't know how to do it.

-- ) VALUES (
-- 'schema_version',
-- '1.0.0'
-- );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

comment this out because the table doesn't have a primary key and diesel doesn't like that. real solution would be to just give it a primary key

time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,
}
}
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 file is generated by the diesel setup. The only one I'm using is project, but I left the rest here to show what it looks like.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Dumb question, but what input file is disel reading to emit this output?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not a dumb question at all, I believe this is generated by talking to the running database. yikes

let manager = r2d2::ConnectionManager::<diesel::PgConnection>::new(
&config.database.url.url(),
);
let dpool = r2d2::Pool::new(manager).unwrap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These two lines took me so long to figure out. The documentation is not so good.

pub time_created: DateTime<Utc>,
pub time_modified: DateTime<Utc>,
pub time_deleted: Option<DateTime<Utc>>,
}
Copy link
Contributor Author

@david-crespo david-crespo Aug 8, 2021

Choose a reason for hiding this comment

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

Note that these properties are not nested under identity like in the existing Project because I don't know how to tell Diesel to do that. It wants columns in the DB to map straight to struct fields. There may well be a way to annotate this so that nesting under identity works.

project
.filter(name.eq(name_.as_str()))
.first::<db::model::DieselProject>(&*conn)
.map_err(|e| e.into())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we import db::diesel_schema::project instead of the dsl thing we have to write

project::table
    .filter(project::columns::name.eq(name.as_str()))

}
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, that might be okay - just means that we can define more featureful converters from DieselError to Error, depending on the type.

I imagine that NotFound triggers more work for the caller to add context, and everything else is more-or-less passthrough.

@@ -43,6 +43,7 @@ pub type CreateResult<T> = Result<T, Error>;
pub type DeleteResult = Result<(), Error>;
/** Result of a list operation that returns an ObjectStream */
pub type ListResult<T> = Result<ObjectStream<T>, Error>;
pub type ListResultVec<T> = Result<Vec<T>, Error>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not 100% sure this is a type we should be adding.

I thought the reason for having ListResult defined like this was to say "if you're going to return a list, do it via an ObjectStream so we can stream the results back to the caller".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, this is just a placeholder to make it work. diesel doesn't support streams

time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Dumb question, but what input file is disel reading to emit this output?

Comment on lines 143 to 144
pub struct DieselProject {
pub id: Uuid,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm assuming that we would probably just end up calling this Project if there's no ambiguity, right? No need to have db::model::Project and db::model::DieselProject simultaneously?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah definitely. This is just to have this working without breaking everything else

@smklein
Copy link
Collaborator

smklein commented Aug 9, 2021

Just pushed the conversions for Name, demonstrating what's necessary to get ToSql / FromSql for one of these "primitive" types. Seems to compose well, and also allows for the "String -> Name" conversion to throw a deserialization error if something goes wrong.

@david-crespo david-crespo mentioned this pull request Aug 9, 2021
Copy link
Collaborator

@smklein smklein left a comment

Choose a reason for hiding this comment

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

So projects + instances are using diesel here, tests are passing, and errors are getting propagated successfully.

This might be a good point to stop and get some preliminary feedback before the rest of the world gets converted: what do we think about the shape of this?

FWIW, if I continue the conversion, I am confident the following files can be removed entirely:

  • omicron-nexus/db/sql_operations.rs
  • omicron-nexus/db/sql.rs
  • omicron-nexus/db/operations.rs
  • omicron-nexus/db/schema.rs

migrations/00000000000000_diesel_initial_setup/up.sql Outdated Show resolved Hide resolved
}
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've gone ahead and updated these with two explicit constructions that provide enough context to pass all tests.

omicron-nexus/src/db/datastore.rs Show resolved Hide resolved
.filter(dsl::time_deleted.is_null())
.limit(pagparams.limit.get().into())
.into_boxed();
let query = match pagparams.direction {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is the "new" version of pagination. Can probably be refactored into a generic function, but the TL;DR is:

  • limit query to pagparams.limit
  • filter based on the marker, if one is supplied
  • apply order to query

diesel::update(dsl::project)
.filter(dsl::time_deleted.is_null())
.filter(dsl::name.eq(name))
.set(&updates)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The neat part about diesel changesets is that they view Option arguments as "basically not supplied", so the whole set of things-which-can-be-changed can just be passed directly here.

Comment on lines +291 to +292
.on_conflict(dsl::id)
.do_nothing()
Copy link
Collaborator

Choose a reason for hiding this comment

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

The implementation of "insert_unique_idempotent_and_fetch" seems pretty trivial here - on conflict, don't do anything, and get the result always.

omicron-nexus/src/db/datastore.rs Outdated Show resolved Hide resolved
let instance = diesel::update(dsl::instance)
.filter(dsl::time_deleted.is_null())
.filter(dsl::id.eq(instance_id))
.filter(dsl::instance_state.eq_any(vec![stopped, failed]))
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are a couple different ways of stating it, but this is a valid way to check for "instance state being in any of the following valid states".

let view_list =
to_list::<db::model::Project, Project>(project_stream).await;
Ok(HttpResponseOk(ScanByNameOrId::results_page(&query, view_list)?))
// TODO filter out non-ok things like to_list does
Copy link
Collaborator

Choose a reason for hiding this comment

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

@david-crespo not sure I understand this comment - projects is already a vec of Projects, so it exclusively contains "OK things".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah ok. If the maybe_object.is_ok() was merely an artifact of unwrapping stuff out of an async stream, then this is not needed.

@smklein
Copy link
Collaborator

smklein commented Aug 26, 2021

FYI, there is a corresponding steno change here: oxidecomputer/steno#11

@smklein
Copy link
Collaborator

smklein commented Aug 30, 2021

FYI, there is a corresponding steno change here: oxidecomputer/steno#11

Ended up closing that change, implementing via newtype pattern within Omicron only.

@david-crespo
Copy link
Contributor Author

since I can't approve my own PR

image

@smklein smklein marked this pull request as ready for review August 31, 2021 21:17
Copy link
Collaborator

@smklein smklein left a comment

Choose a reason for hiding this comment

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

Nice job, @david-crespo 😁

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.

3 participants