-
Notifications
You must be signed in to change notification settings - Fork 40
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
Conversation
} | ||
} | ||
} | ||
|
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 probably won't work out because in order to produce our Error
, we need other info from context that DieselError
can't give us
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, 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.
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 gone ahead and updated these with two explicit constructions that provide enough context to pass all tests.
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 great! Exactly what I was picturing, but I didn't know how to do it.
omicron-common/src/sql/dbinit.sql
Outdated
-- ) VALUES ( | ||
-- 'schema_version', | ||
-- '1.0.0' | ||
-- ); |
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.
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>, | ||
} | ||
} |
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 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.
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.
Dumb question, but what input file is disel reading to emit this output?
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.
not a dumb question at all, I believe this is generated by talking to the running database. yikes
omicron-nexus/src/lib.rs
Outdated
let manager = r2d2::ConnectionManager::<diesel::PgConnection>::new( | ||
&config.database.url.url(), | ||
); | ||
let dpool = r2d2::Pool::new(manager).unwrap(); |
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.
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>>, | ||
} |
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.
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.
omicron-nexus/src/nexus.rs
Outdated
project | ||
.filter(name.eq(name_.as_str())) | ||
.first::<db::model::DieselProject>(&*conn) | ||
.map_err(|e| e.into()) |
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.
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()))
d1ef487
to
ab3ab9e
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.
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>; |
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'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".
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, this is just a placeholder to make it work. diesel doesn't support streams
time_modified -> Timestamptz, | ||
time_deleted -> Nullable<Timestamptz>, | ||
} | ||
} |
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.
Dumb question, but what input file is disel reading to emit this output?
omicron-nexus/src/db/model.rs
Outdated
pub struct DieselProject { | ||
pub id: Uuid, |
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'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?
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 definitely. This is just to have this working without breaking everything else
Just pushed the conversions 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.
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
} | ||
} | ||
} | ||
|
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 gone ahead and updated these with two explicit constructions that provide enough context to pass all tests.
omicron-nexus/src/db/datastore.rs
Outdated
.filter(dsl::time_deleted.is_null()) | ||
.limit(pagparams.limit.get().into()) | ||
.into_boxed(); | ||
let query = match pagparams.direction { |
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 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
omicron-nexus/src/db/datastore.rs
Outdated
diesel::update(dsl::project) | ||
.filter(dsl::time_deleted.is_null()) | ||
.filter(dsl::name.eq(name)) | ||
.set(&updates) |
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 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.
.on_conflict(dsl::id) | ||
.do_nothing() |
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 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
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])) |
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 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 |
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.
@david-crespo not sure I understand this comment - projects
is already a vec of Projects
, so it exclusively contains "OK things".
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 ok. If the maybe_object.is_ok()
was merely an artifact of unwrapping stuff out of an async stream, then this is not needed.
FYI, there is a corresponding steno change here: oxidecomputer/steno#11 |
Ended up closing that change, implementing via newtype pattern within Omicron only. |
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.
Nice job, @david-crespo 😁
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.
Name
workString(63)
instead ofText
)diesel::result::Error
into ourError
, 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.sql_update_precond