From 8a83a34eb859a9d11189cb3f42f9b9210cae5d61 Mon Sep 17 00:00:00 2001 From: James Pace Date: Fri, 16 Feb 2024 01:51:14 +0000 Subject: [PATCH] Finish sqlx draft. --- _drafts/sqlx-without-macros.md | 235 ++++++++++++++++++++++++++++++++ _drafts/sqlx-without-macros.qmd | 74 ---------- 2 files changed, 235 insertions(+), 74 deletions(-) create mode 100644 _drafts/sqlx-without-macros.md delete mode 100644 _drafts/sqlx-without-macros.qmd diff --git a/_drafts/sqlx-without-macros.md b/_drafts/sqlx-without-macros.md new file mode 100644 index 0000000..6df0603 --- /dev/null +++ b/_drafts/sqlx-without-macros.md @@ -0,0 +1,235 @@ +--- +title: "Rust: sqlx without macros" +author: "James Pace" +date: "2024/02/15" +--- + +[`sqlx`](https://github.com/launchbadge/sqlx) is a Rust crate that allows +for interacting with a SQL database. +One of the more popular features of `sqlx` is that it does compile time checking +of database queries (giving you some ORM-ish properties) while still allowing +you to write straight SQL queries without the abstraction of an ORM though use +of their [`query!`](https://docs.rs/sqlx/0.5.5/sqlx/macro.query.html). + +To make this work, `sqlx` requires: + +1. access to a database at compile time, OR +2. config files to be generated from a running database everytime + the database queries are modified. + +I don't particularly like either of those options, though I admittedly +have not tried either of them long term. +Requring a running database at compile time would seem to make compilation +a lot slower. +It also adds a unexpected step (run a container hosting a development database) +before you can compile, which just feels wrong. +The configuation files are better in the sense they don't require a running +database at compile time, but it's an extra thing that has to be kept in sync +with the code. + +Luckily, `sqlx` provides functions that can be used to do queries that aren't +checked at compile time for people with the same concerns as I have. +Because this isn't the recommended path though, the number of examples of +using the functions online is a little lacking, which I'll make an effort +to improve in this post. + +The other thing I'll show in this post is working with JSON in Postgres. +Postgres has native support for working with JSON documents, providing +a lot of the benefits of NoSQL databases, but in a SQL database. +`sqlx`'s API for json documents integrates well with serde, and is resonably +easy to work with. + +# Pre-reqs + +Running the example requires a running Postgres database, which I'm going to run +locally via [`podman`](https://podman.io/) using the bash script below. + +```bash +#!/usr/bin/env bash + +DATA_DIR="$PWD/data" + +if [ ! -d "$DATA_DIR" ]; then + echo "Making data directory." + mkdir $DATA_DIR +fi +podman run --rm --name test-db --network=host \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=devpassword \ + -e PGDATA=/var/lib/postgresql/data/pgdata \ + -v $DATA_DIR:/var/lib/postgresql/data \ + docker.io/library/postgres:latest +``` + +The script: + +1. Defines `DATA_DIR` which will be a local host directory we will mount + in the database container and tell the database to save its files to + so they persist beyond container restarts. +2. Makes that directory if it is missing. +3. Starts a container running the official postgres image. + Environment variables are used to set some settings to values suitable + for local testing. + +# Example + +For the rest of this post, I will walk through our example (which can be found +[here](https://git.jpace121.net/public/sql-fun/src/branch/main/src/main.rs). + +``` rust +use serde::{Deserialize, Serialize}; +use sqlx::{types::Json, Row}; +``` +We start by "use"-ing some structs and traits we're going to +want later, starting with the standard `serde` ones. +"Use"-ing `sqlx::Row` is important because it brings the traits we +need to work with the results of doing the sqlx queries into scope. + +```rust +#[derive(Serialize, Deserialize, Debug)] +struct Doc { + age: u32, + fav_color: String, + fav_movie: String, +} +``` +We next define a `Doc` struct that we will work with like JSON +in the database. + +Starting with our main function... + +```rust +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Connect to the database. + let pool = + sqlx::postgres::PgPool::connect("postgresql://postgres:devpassword@localhost/postgres") + .await?; +``` +The first thing we do in main is to connect to the database by making a connection pool. +The `connect` function takes a string argument to control what database by what method +we want to connect to. +Specifically, in the call above, we're going to connect as the user `postgres` using the +password `devpassword` to the server running on `localhost` with the default port, to the +database `postgres`. + +```rust + // Make the table. + let _ = sqlx::query( + "CREATE TABLE IF NOT EXISTS test (id SERIAL PRIMARY KEY, + name TEXT, + doc JSONB + );", + ) + .execute(&pool) + .await?; +``` +The first thing we're going to do is generate a table for us +to work with in the rest of example. +This is the first time we've seen the `sqlx::query` function, +so we"ll pause here to talk about it. +The input to the query function is a string with, well, a SQL query +in it. +`query` returns a `Query` object which has member functions we call +to actually do the database call. +The two functions we'll use in this example are `execute` and `fetch_one`. +Both functions take the `pool` as an input an return a future with a result +that we need to await. +What's in the non-Err part of the results depends on which function we call. +For `execute` it is `()`, and thus we call `execute` when we're not expecting to +be given anything by the database. +For `fetch_one` it is a `sqlx::Row`, which contains members we can call to +query the output of our database query. + +```rust + // Insert some entries into db. + // Add James. + let james_doc = Doc { + age: 30, + fav_color: "blue".to_owned(), + fav_movie: "Iron Man".to_owned(), + }; + let james_doc_as_str = serde_json::to_string(&james_doc)?; + let james_add_query = format!( + r#"INSERT INTO test (name, doc) VALUES('James', '{}');"#, + james_doc_as_str + ); + let _ = sqlx::query(&james_add_query).execute(&pool).await?; +``` +The next thing we do is add rows to table so we have something to query. +We start by instantiating and instance of our `Doc` struct, and then using +`serde_json` to serialize it into a JSON string. +We generate and execute the database query as before, with the exception that +we make the string we're going to pass into `sqlx::query` in its own line. +One thing of interest is we define the string using a "raw string literal" +(the `r#` stuff). +The official documentation for the "raw string literal"s is +[here](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals). +It's basically a special ysntax that allows us to avoid having to escape characters +inside of the string. + +The next block of text is basically the same as above, but for our second user, +named "Dave". + +```rust + // Add Dave. + let dave_doc = Doc { + age: 25, + fav_color: "yellow".to_owned(), + fav_movie: "Spiderman".to_owned(), + }; + let dave_doc_as_str = serde_json::to_string(&dave_doc)?; + let dave_add_query = format!( + r#"INSERT INTO test (name, doc) VALUES('Dave', '{}');"#, + dave_doc_as_str + ); + let _ = sqlx::query(&dave_add_query).execute(&pool).await?; +``` +We're now ready to query the database: +```bash + // Get james favorite color. + let fav_color_query = r#"SELECT doc->>'fav_color' FROM test WHERE name = 'James';"#; + let fav_color_result = sqlx::query(&fav_color_query).fetch_one(&pool).await?; + let james_fav_color = fav_color_result.try_get::(0)?; + println!("James' Fav Color: {}", james_fav_color); +``` +Our first query is to find James's favorite color, which is saved in the `Doc` in +James' row. +There are three interesting new things in this block of code: + +1. In the SQL query, we're going to use the special syntax `->>` to refer + to elements inside the JSON object we added to the database. +2. Since we're trying to get one element from the database, instead of + call `execute` on the query, we use `fetch_one`. +3. The result of `fetch_one` is a `sqlx::Row`. + To get actual data out of the row we call `try_get`, which takes one + parameter, the index of the element of the row to get an elemet of. + There are two type parameters for `try_get`, the first the type of the + thing we're getting from the row and the second the type of index. + +```rust + // Get name based on movie. + let spiderman_query = r#"SELECT name FROM test WHERE doc->>'fav_movie' = 'Spiderman';"#; + let spiderman_result = sqlx::query(&spiderman_query).fetch_one(&pool).await?; + let spiderman_name = spiderman_result.try_get::(0)?; + println!("{} likes Spiderman.", spiderman_name); +``` + +Our second example is similar to the first, but we use the `->>` syntax to pull +something out the json doc in the `WHERE` condition instead of in what we `SELECT`. + + +```rust + // Get james's doc. + let james_query = r#"SELECT doc FROM test where name = 'James'"#; + let james_result = sqlx::query(&james_query).fetch_one(&pool).await?; + let james_doc = james_result.try_get::, usize>(0)?.0; + println!("James Doc is: {:?}", james_doc); + + Ok(()) +} +``` + +In our final example, we pull out the whole `Doc` for James, using +the `Json` type from `sqlx` to convert between the JSON data in the +database and our `Doc` struct. diff --git a/_drafts/sqlx-without-macros.qmd b/_drafts/sqlx-without-macros.qmd deleted file mode 100644 index 405345f..0000000 --- a/_drafts/sqlx-without-macros.qmd +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: "Rust: sqlx without macros" -author: "James Pace" -date: "2024/01/01" ---- - -[`sqlx`](https://github.com/launchbadge/sqlx) is a Rust crate that allows -for interacting with a SQL database. -One of the more popular features of `sqlx` is that it does compile time checking -of database queries (giving you some ORM-ish properties) while still allowing -you to write straight SQL queries without the abstraction of an ORM though use -of their [`query!`](https://docs.rs/sqlx/0.5.5/sqlx/macro.query.html). - -To make this work, `sqlx` requires: - -1. access to a database at compile time, OR -2. config files to be generated from a running database everytime - the database queries are modified. - -I don't particularly like either of those options, though I admittedly -have not tried either of them long term. -Requring a running database at compile time would seem to make compilation -a lot slower. -It also adds a unexpected step (run a container hosting a development database) -before you can compile, which just feels wrong. -The configuation files are better in the sense they don't require a running -database at compile time, but it's an extra thing that has to be kept in sync -with the code. - -Luckily, `sqlx` provides functions that can be used to do queries that aren't -checked at compile time for people with the same concerns as I have. -Because this isn't the recommended path though, the number of examples of -using the functions online is a little lacking, which I'll make an effort -to improve in this post. - -The other thing I'll show in this post is working with JSON in Postgres. -Postgres has native support for working with JSON documents, providing -a lot of the benefits of NoSQL databases, but in a SQL database. -`sqlx`'s API for json documents integrates well with serde, and is resonably -easy to work with. - -# Pre-reqs - -Running the example requires a running Postgres database, which I'm going to run -locally via [`podman`](https://podman.io/) using the bash script below. - -```bash -#!/usr/bin/env bash - -DATA_DIR="$PWD/data" - -if [ ! -d "$DATA_DIR" ]; then - echo "Making data directory." - mkdir $DATA_DIR -fi -podman run --rm --name test-db --network=host \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=devpassword \ - -e PGDATA=/var/lib/postgresql/data/pgdata \ - -v $DATA_DIR:/var/lib/postgresql/data \ - docker.io/library/postgres:latest -``` - -The script: - -1. Defines `DATA_DIR` which will be a local host directory we will mount - in the database container and tell the database to save its files to - so they persist beyond container restarts. -2. Makes that directory if it is missing. -3. Starts a container running the official postgres image. - Environment variables are used to set some settings to values suitable - for local testing. - -# Example