Database queries with Diesel#192
Conversation
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
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.
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.
This is great! Exactly what I was picturing, but I didn't know how to do it.
| -- ) VALUES ( | ||
| -- 'schema_version', | ||
| -- '1.0.0' | ||
| -- ); |
There was a problem hiding this comment.
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.
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.
Dumb question, but what input file is disel reading to emit this output?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
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()) |
There was a problem hiding this comment.
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.
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.
| 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.
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.
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.
Dumb question, but what input file is disel reading to emit this output?
| pub struct DieselProject { | ||
| pub id: Uuid, |
There was a problem hiding this comment.
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.
Yeah definitely. This is just to have this working without breaking everything else
|
Just pushed the conversions for |
smklein
left a comment
There was a problem hiding this comment.
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.
I've gone ahead and updated these with two explicit constructions that provide enough context to pass all tests.
| .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.
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 filterbased on the marker, if one is supplied- apply
orderto query
| diesel::update(dsl::project) | ||
| .filter(dsl::time_deleted.is_null()) | ||
| .filter(dsl::name.eq(name)) | ||
| .set(&updates) |
There was a problem hiding this comment.
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.
The implementation of "insert_unique_idempotent_and_fetch" seems pretty trivial here - on conflict, don't do anything, and get the result always.
| 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.
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.
@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.
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. |
smklein
left a comment
There was a problem hiding this comment.
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
masterbecause 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.
NameworkString(63)instead ofText)diesel::result::Errorinto 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