From 6f6b04d3c223f3995dbc0c7a54f8bb4a87387fed Mon Sep 17 00:00:00 2001 From: "Peter C. S. Scholtens" Date: Fri, 17 Nov 2023 17:11:56 +0100 Subject: [PATCH 1/6] Example and step-by-step explanation added for composite-types in PostgreSQL. --- .github/workflows/ci.yml | 1 + Cargo.toml | 1 + examples/postgres/composite_types/Cargo.toml | 10 + examples/postgres/composite_types/README.md | 185 ++++++++++++++++++ examples/postgres/composite_types/diesel.toml | 9 + .../examples/composite2rust_colors.rs | 80 ++++++++ .../examples/composite2rust_coordinates.rs | 63 ++++++ .../down.sql | 6 + .../up.sql | 36 ++++ .../down.sql | 6 + .../up.sql | 63 ++++++ .../down.sql | 5 + .../up.sql | 77 ++++++++ examples/postgres/composite_types/src/lib.rs | 14 ++ .../postgres/composite_types/src/schema.rs | 21 ++ 15 files changed, 577 insertions(+) create mode 100644 examples/postgres/composite_types/Cargo.toml create mode 100644 examples/postgres/composite_types/README.md create mode 100644 examples/postgres/composite_types/diesel.toml create mode 100644 examples/postgres/composite_types/examples/composite2rust_colors.rs create mode 100644 examples/postgres/composite_types/examples/composite2rust_coordinates.rs create mode 100644 examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/down.sql create mode 100644 examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/up.sql create mode 100644 examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/down.sql create mode 100644 examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/up.sql create mode 100644 examples/postgres/composite_types/src/lib.rs create mode 100644 examples/postgres/composite_types/src/schema.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0e2281e08b9..dd995d8877f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -371,6 +371,7 @@ jobs: cargo +stable clippy --tests --manifest-path examples/postgres/all_about_inserts/Cargo.toml cargo +stable clippy --tests --manifest-path examples/postgres/all_about_updates/Cargo.toml cargo +stable clippy --tests --manifest-path examples/postgres/custom_types/Cargo.toml + cargo +stable clippy --tests --manifest-path examples/postgres/composite_types/Cargo.toml cargo +stable clippy --tests --manifest-path examples/postgres/relations/Cargo.toml cargo +stable clippy --tests --manifest-path diesel_derives/Cargo.toml --features "sqlite diesel/sqlite" diff --git a/Cargo.toml b/Cargo.toml index ff383355fdb7..e1fb4f5fb5b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "examples/postgres/getting_started_step_2", "examples/postgres/getting_started_step_3", "examples/postgres/custom_types", + "examples/postgres/composite_types", "examples/postgres/relations", "examples/sqlite/all_about_inserts", "examples/sqlite/getting_started_step_1", diff --git a/examples/postgres/composite_types/Cargo.toml b/examples/postgres/composite_types/Cargo.toml new file mode 100644 index 000000000000..67b167d52732 --- /dev/null +++ b/examples/postgres/composite_types/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "diesel-postgres-composite-type" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +diesel = { version = "2.1", features = ["postgres" ] } +dotenvy = "0.15" diff --git a/examples/postgres/composite_types/README.md b/examples/postgres/composite_types/README.md new file mode 100644 index 000000000000..043163e387fa --- /dev/null +++ b/examples/postgres/composite_types/README.md @@ -0,0 +1,185 @@ +# diesel-postgres-composite-type +This repository contains a series of examples to demonstrate how [composite types](https://www.postgresql.org/docs/current/rowtypes.html) in PostgreSQL can +be used in the diesel query builder. + +This manual assumes you're familiar with [PostgreSQL](https://www.postgresql.org/docs/) +and [Rust](https://www.rust-lang.org/learn). As I struggled to understand +how you can use [Diesel](https://diesel.rs) with PostgreSQL's composite types, +I have written a series of examples which stepwise introduces the required +methods and traits to deal with this. + +What will be discussed? +* Importing data from postgreSQL to Rust [anonymously](README.md#from-pgsql-to-rust-anonymously-coordinates). +* Importing data from postgreSQL to Rust [using named fields](README.md#from-pgsql-to-rust-with-type-binding-colors). +* Exporting data from Rust to PostgreSQL (TODO) + + +# From pgSQL to Rust anonymously: coordinates. +Let's start with a simple table containing an unique identifier and two columns +with integers. After downloading the repository and running `diesel migration run` +you should be able to see the following, using e.g. the terminal `psql`: + +```sql +composite_type_db=# SELECT * FROM coordinates; + coord_id | xcoord | imaginairy_part +----------+-----------+----------------- + 1 | 1 | 0 + 2 | 0 | 1 + 3 | 1 | 1 + 4 | 3 | 4 +``` +### Get the used types from the column definition +The typical working flow when using Diesel is to automatically generate [schema.rs](./src/schema.rs) which +provides us with the type information of the columns which are present in +our coordinates table. Also, an SQL function, [distance_from_origin()](./migrations/2023-10-23-111951_composite2rust_coordinates/up.sql), +is defined. We need to explain this to the Rust compiler using the [sql_function!](https://docs.rs/diesel/latest/diesel/expression/functions/macro.sql_function.html) +macro like this: +```rust +sql_function!(fn distance_from_origin(re: Integer,im: Integer) -> Float); +``` +Keep in mind that we specify [only postgreSQL types](https://docs.rs/diesel/latest/diesel/sql_types/index.html) +as the input parameters and return value(s) of this function. If the columns +names are also *in scope* then we can write in Rust: + +```rust +let results: Vec<(i32, f32)> = coordinates + .select((coord_id, distance_from_origin(xcoord, ycoord))) + .load(connection)?; +``` +So we expect a vector of a 2-tuple, or ordered pair of the Rust types ```i32``` +and ```f32```. Mind that the float type is not present in the table but is +specified in by the SQL function in the database and also in the macro `sql_function!` +definition above. Of course we can expand this to very long tuples, but that +will become error prone as we have to specify the sequence of type correctly +every function call. Try out the [first example](./examples/composite2rust_coordinates) with: + +```sh +cargo run --example composite2rust_coordinates +``` + +### Define an alias type +To avoid errors we could define an [alias type](https://doc.rust-lang.org/stable/std/keyword.type.html) +once and use this in the various calls of our function. +```rust +type Distance = (i32, f32); +``` +The re-written function call will then look like: +```rust +let results: Vec = coordinates + .select((coord_id, distance_from_origin(xcoord, ycoord))) + .load(connection)?; +``` + +### Reducing the output to a single value instead of an array +The default output of a query to the database is a table with results. +However, frequenty we may only expect a single answer, especially if a function +has defined several **OUT**-*put* parameters instead of returning a table, like the +created SQL function ```shortest_distance()```. +To avoid the destructering of the vector, when a vector is not needed, we +can use the ```get_result()``` instead of the ```load()``` function call +with our specified type: + +```rust +let result: Distance = select(shortest_distance()) + .get_result(connection)?; +``` + +### Creating a type in PostgreSQL world +Using a tuple only enforces the correct number of return values and their basic type. +If multiple values of the same type are returned, they can easily be mixed-up without +any warning. Therefore, to improve readability of the database SQL functions it makes +sense to introduce new types, like for example this one: +```sql +CREATE TYPE my_comp_type AS (coord_id INTEGER, distance FLOAT4); +``` +If we specified a database function ```longest_distance()``` we can simply +use that now on the Rust side with: + +```rust +let result: Distance = select(longest_distance()) + .get_result(connection)?; +``` +So, although we specified a new type in the database, we **don't need** to +specify it on the Rust side too. If we never make errors, that would be a +possible solution. However, like unsafe Rust, this is not recommended. Why +build in possible pitfalls if we can avoid them? + +# From pgSQL to Rust with type binding: colors. +In the [second example](./examples/composite2rust_colors.rs) we want to convert any RGB value, consisting of three integer values, to a new type which expresses the reflected light and suggests a name for this reflection: +```sql +CREATE TYPE gray_type AS (intensity FLOAT4, suggestion TEXT); +``` +This new type will be used in two exactly the same SQL functions `color2grey()` and `color2gray()` of which input and return value are specified like: +```sql +CREATE FUNCTION color2grey( + red INTEGER, + green INTEGER, + blue INTEGER +) RETURNS gray_type AS +$$ +... +``` +You can run the example with the following command: +```sh +cargo run --example composite2rust_colors +``` +On the Rust side, we define the interpretation of both functions differently, the first one using a tuple similar to the coordinates example, the second one using a _locally_ defined Rust type for interpreting a tuple: notice the **Pg**-prefix of `PgGrayType`. + +```rust +sql_function!(fn color2grey(r: Integer, g: Integer,b: Integer) -> Record<(Float,Text)>); +sql_function!(fn color2gray(r: Integer, g: Integer,b: Integer) -> PgGrayType); +``` +As this only creates a type with anonymous fields, which can be adressed by their field number **object.0**, **object.1** etc., it would be more convenient to attach names to the fields. Therefore we need to define a type with our intended field names, which we can use _globally_ (or at least outside the database related code space): +```rust +#[derive(Debug, FromSqlRow)] +pub struct GrayType { + pub intensity: f32, + pub suggestion: String, +} +``` +The derived [FromSqlRow](https://docs.rs/diesel/latest/diesel/deserialize/trait.FromSqlRow.html) trait explains Diesel it is allowed to convert a tuple to this new type. We only need a implementation on _how_ to do that for a PostgreSQL backend: +```rust +impl FromSql for GrayType { + fn from_sql(bytes: PgValue) -> deserialize::Result { + let (intensity, suggestion) = FromSql::, Pg>::from_sql(bytes)?; + Ok(GrayType { + intensity, + suggestion, + }) + } +} +``` +Although this seems trivial for this example, it also allows the posssibility to add some more checks or modifications on the imported data: we could for example limit the values of intensity between 0 and 100%. + + +Did you read the [License](./LICENSE)? + + + + + + + + +# Miscellaneous, Set-up etc. +Switch to user postgres with the following terminal command: +```bash + su - postgres + psql +``` +In this psql terminal do: +```sql +CREATE DATABASE composite_type_db ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' template=template0 OWNER postgres; +``` +this should reply with: +``` +CREATE DATABASE +``` +You can verify the list of present databases with typing `\l` and then exit with `\q` + + echo DATABASE_URL=postgres://username:password@localhost/diesel_demo > .env + +Create it with the diesel command (will create database if it didn't exist, but with your locale settings.): + diesel setup + +composite_type_db diff --git a/examples/postgres/composite_types/diesel.toml b/examples/postgres/composite_types/diesel.toml new file mode 100644 index 000000000000..c028f4a6aa1a --- /dev/null +++ b/examples/postgres/composite_types/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId"] + +[migrations_directory] +dir = "migrations" diff --git a/examples/postgres/composite_types/examples/composite2rust_colors.rs b/examples/postgres/composite_types/examples/composite2rust_colors.rs new file mode 100644 index 000000000000..c332396fc9f3 --- /dev/null +++ b/examples/postgres/composite_types/examples/composite2rust_colors.rs @@ -0,0 +1,80 @@ +// Function to connect to database. +use diesel_postgres_composite_type::establish_connection; + +// Bring column names of the table into scope +use diesel_postgres_composite_type::schema::colors::{ + blue, color_id, color_name, dsl::colors, green, red, +}; + +// Define the signature of the SQL function we want to call: +use diesel::pg::Pg; +use diesel::pg::PgValue; +use diesel::sql_function; +use diesel::sql_types::{Float, Integer, Record, Text}; +sql_function!(fn color2grey(r: Integer, g: Integer,b: Integer) -> Record<(Float,Text)>); +sql_function!(fn color2gray(r: Integer, g: Integer,b: Integer) -> PgGrayType); + +// Needed to select, construct the query and submit it. +use diesel::deserialize::{self, FromSql, FromSqlRow}; +use diesel::{QueryDsl, RunQueryDsl}; + +#[derive(Debug, FromSqlRow)] +pub struct GrayType { + pub intensity: f32, + pub suggestion: String, +} + +// Define how a record of this can be converted to a Postgres type. +type PgGrayType = Record<(Float, Text)>; + +// Explain how this Postgres type can be converted to a Rust type. +impl FromSql for GrayType { + fn from_sql(bytes: PgValue) -> deserialize::Result { + let (intensity, suggestion) = FromSql::, Pg>::from_sql(bytes)?; + Ok(GrayType { + intensity, + suggestion, + }) + } +} + +fn main() { + let connection = &mut establish_connection(); + // Experiment 1: Define a type for clearer re-use, + // similar as in the coordinates example. + type Color = (i32, i32, i32, i32, Option); + let results: Vec = colors + .select((color_id, red, green, blue, color_name)) + .load(connection) + .expect("Error loading colors"); + for r in results { + println!( + "index {:?}, red {:?}, green {:?}, blue {:?}, name: {:?}", + r.0, r.1, r.2, r.3, r.4 + ); + } + // Experiment 2: When recognizing the new type with named fields, + // the code is more readable. + let results: Vec<(i32, GrayType)> = colors + .select((color_id, color2grey(red, green, blue))) + .load(connection) + .expect("Error loading gray conversions"); + for (i, g) in results { + println!( + "Color {:?} has intensity level {:?} with suggested name {:?}", + i, g.intensity, g.suggestion + ); + } + // Experiment 3: Similar, using the type also in the above listed + // sql_function!(...) definition. + let results: Vec<(i32, GrayType)> = colors + .select((color_id, color2gray(red, green, blue))) + .load(connection) + .expect("Error loading gray conversions"); + for (i, g) in results { + println!( + "Color {:?} has intensity level {:?} with suggested name {:?}", + i, g.intensity, g.suggestion + ); + } +} diff --git a/examples/postgres/composite_types/examples/composite2rust_coordinates.rs b/examples/postgres/composite_types/examples/composite2rust_coordinates.rs new file mode 100644 index 000000000000..a785bdd6ed29 --- /dev/null +++ b/examples/postgres/composite_types/examples/composite2rust_coordinates.rs @@ -0,0 +1,63 @@ +// Function to connect to database. +use diesel_postgres_composite_type::establish_connection; + +// Bring column names of the table into scope +use diesel_postgres_composite_type::schema::coordinates::{ + coord_id, dsl::coordinates, xcoord, ycoord, +}; + +// Define the signature of the SQL function we want to call: +use diesel::sql_function; +use diesel::sql_types::Integer; +sql_function!(fn distance_from_origin(re: Integer,im: Integer) -> Float); +sql_function!(fn shortest_distance() -> Record<(Integer,Float)>); +sql_function!(fn longest_distance() -> Record<(Integer,Float)>); + +// Needed to select, construct the query and submit it. +use diesel::select; +use diesel::{QueryDsl, RunQueryDsl}; + +fn main() { + let connection = &mut establish_connection(); + // Experiment 1: Read tuple directly from processed table + let results: Vec<(i32, f32)> = coordinates + .select((coord_id, distance_from_origin(xcoord, ycoord))) + .load(connection) + .expect("Error loading numbers"); + for r in results { + println!("index {:?}, length {:?}", r.0, r.1); + } + // Experiment 2: Define a type for clearer re-use + type Distance = (i32, f32); + let results: Vec = coordinates + .select((coord_id, distance_from_origin(xcoord, ycoord))) + .load(connection) + .expect("Error loading numbers"); + for r in results { + println!("index {:?}, length {:?}", r.0, r.1); + } + // Experiment 3: use tuple for single result and do some math in SQL + // Notice that we only expect one result, not an vector + // of results, so use get_result() instead of load()) + let result: Distance = select(shortest_distance()) + .get_result(connection) + .expect("Error loading longest distance"); + println!( + "Coordinate {:?} has shortest distance of {:?}", + result.0, result.1 + ); + // Unfortunately, the members of our Distance struct, a tuple, are anonymous. + // Will be unhandy for longer tuples. + + // Experiment 4: use composite type in SQL, read as Record in Rust + // Notice that we only expect one result, not an vector + // of results, so use get_result() instead of load()) + let result: Distance = select(longest_distance()) + .get_result(connection) + .expect("Error loading longest distance"); + println!( + "Coordinate {:?} has longest distance of {:?}", + result.0, result.1 + ); + // TODO: also show an example with a recursively interpreted Record> +} diff --git a/examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/down.sql b/examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 000000000000..a9f526091194 --- /dev/null +++ b/examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/up.sql b/examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 000000000000..d68895b1a7b7 --- /dev/null +++ b/examples/postgres/composite_types/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/down.sql b/examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/down.sql new file mode 100644 index 000000000000..2cd2311f4528 --- /dev/null +++ b/examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +DROP TABLE coordinates; +DROP FUNCTION distance_from_origin; +DROP FUNCTION shortest_distance; +DROP FUNCTION longest_distance; +DROP TYPE my_comp_type; diff --git a/examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/up.sql b/examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/up.sql new file mode 100644 index 000000000000..918965f3bf80 --- /dev/null +++ b/examples/postgres/composite_types/migrations/2023-10-23-111951_composite2rust_coordinates/up.sql @@ -0,0 +1,63 @@ +-- Define the contents of the table. +CREATE TABLE coordinates ( + coord_id SERIAL PRIMARY KEY, + xcoord INTEGER NOT NULL DEFAULT 0, + ycoord INTEGER NOT NULL DEFAULT 0 +); + +-- Fill table with some example data. +INSERT INTO coordinates (xcoord, ycoord) VALUES (1, 0); +INSERT INTO coordinates (xcoord, ycoord) VALUES (0, 1); +INSERT INTO coordinates (xcoord, ycoord) VALUES (1, 1); +INSERT INTO coordinates (xcoord, ycoord) VALUES (3, 4); + +-- You can verify the function below in psql with: +-- SELECT distance_from_origin(xcoord, ycoord) FROM coordinates; +CREATE FUNCTION distance_from_origin ( + re INTEGER, + im INTEGER) + RETURNS FLOAT4 AS +$$ +DECLARE + dist FLOAT4; +BEGIN + dist = SQRT(POWER(re,2) + POWER(im,2)); + RETURN dist; +END +$$ LANGUAGE plpgsql IMMUTABLE; + +-- You can verify the function below in psql with: +-- SELECT shortest_distance(); +-- Which should report '(1,1)' or '(2,1)' given the above inserted data. +CREATE FUNCTION shortest_distance ( + OUT _coord_id INTEGER, + OUT distance FLOAT4) + AS +$$ +BEGIN + SELECT coord_id, distance_from_origin(xcoord, ycoord) + FROM coordinates ORDER BY distance_from_origin ASC + INTO _coord_id, distance; +END +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Make a composite type in SQL +CREATE TYPE my_comp_type AS (coord_id INTEGER, distance FLOAT4); + +-- You can verify the function below in psql with: +-- SELECT longest_distance(); +-- Which should report '(4,5)' given the above inserted data. +CREATE FUNCTION longest_distance ( + OUT res my_comp_type) + AS +$$ +DECLARE + _coord_id INTEGER; + distance FLOAT4; +BEGIN + SELECT coord_id, distance_from_origin(xcoord, ycoord) + FROM coordinates ORDER BY distance_from_origin DESC + INTO _coord_id, distance; + res := ROW(_coord_id, distance); +END +$$ LANGUAGE plpgsql IMMUTABLE; diff --git a/examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/down.sql b/examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/down.sql new file mode 100644 index 000000000000..fecebc37fc76 --- /dev/null +++ b/examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +DROP TABLE colors; +DROP FUNCTION color2grey; +DROP FUNCTION color2gray; +DROP TYPE gray_type; diff --git a/examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/up.sql b/examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/up.sql new file mode 100644 index 000000000000..ae6c9e67fa5b --- /dev/null +++ b/examples/postgres/composite_types/migrations/2023-10-25-084848_composite2rust_colors/up.sql @@ -0,0 +1,77 @@ +-- Define the contents of the table. +CREATE TABLE colors ( + color_id SERIAL PRIMARY KEY, + red INTEGER NOT NULL DEFAULT 0, + green INTEGER NOT NULL DEFAULT 0, + blue INTEGER NOT NULL DEFAULT 0, + color_name TEXT +); + +-- Fill table with some example data from /etc/X11/rgb.txt +INSERT INTO colors (red, green,blue, color_name) VALUES (139, 71, 137, 'orchid4'); +INSERT INTO colors (red, green,blue, color_name) VALUES (139, 123, 139, 'thistle4'); +INSERT INTO colors (red, green,blue, color_name) VALUES (205, 41, 144, 'maroon3'); +INSERT INTO colors (red, green,blue, color_name) VALUES (205, 79, 57, 'tomato3'); +INSERT INTO colors (red, green,blue, color_name) VALUES (238, 130, 98, 'salmon2'); +INSERT INTO colors (red, green,blue, color_name) VALUES (238, 216, 174, 'wheat2'); +INSERT INTO colors (red, green,blue, color_name) VALUES (205, 198, 115, 'khaki3'); +INSERT INTO colors (red, green,blue, color_name) VALUES (118, 238, 198, 'aquamarine'); + +-- Make a composite type in SQL +CREATE TYPE gray_type AS (intensity FLOAT4, suggestion TEXT); + +-- Converts an color to a grey value and suggest a name. +CREATE FUNCTION color2grey( + red INTEGER, + green INTEGER, + blue INTEGER +) RETURNS gray_type AS +$$ +DECLARE + intensity FLOAT4; + percentage FLOAT4; + suggestion TEXT; + rec gray_type; +BEGIN + intensity := (red::FLOAT4 + green::FLOAT4 + blue::FLOAT4 ) / 3; + percentage := (100 * intensity / 256)::INTEGER; + IF (percentage > 99) THEN + percentage := 99; + ELSE + IF (percentage < 0) THEN + percentage := 0; + END IF; + END IF; + suggestion := 'grey' || percentage::TEXT; + rec := ROW(intensity,suggestion); + RETURN rec; +END +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Converts an color to a gray value and suggest a name (same but 'e' -> 'a'). +CREATE FUNCTION color2gray( + red INTEGER, + green INTEGER, + blue INTEGER +) RETURNS gray_type AS +$$ +DECLARE + intensity FLOAT4; + percentage FLOAT4; + suggestion TEXT; + rec gray_type; +BEGIN + intensity := (red::FLOAT4 + green::FLOAT4 + blue::FLOAT4 ) / 3; + percentage := (100 * intensity / 256)::INTEGER; + IF (percentage > 99) THEN + percentage := 99; + ELSE + IF (percentage < 0) THEN + percentage := 0; + END IF; + END IF; + suggestion := 'gray' || percentage::TEXT; + rec := ROW(intensity,suggestion); + RETURN rec; +END +$$ LANGUAGE plpgsql IMMUTABLE; diff --git a/examples/postgres/composite_types/src/lib.rs b/examples/postgres/composite_types/src/lib.rs new file mode 100644 index 000000000000..f534ba493aae --- /dev/null +++ b/examples/postgres/composite_types/src/lib.rs @@ -0,0 +1,14 @@ +pub mod schema; + +use diesel::pg::PgConnection; +use diesel::prelude::*; +use dotenvy::dotenv; +use std::env; + +pub fn establish_connection() -> PgConnection { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} diff --git a/examples/postgres/composite_types/src/schema.rs b/examples/postgres/composite_types/src/schema.rs new file mode 100644 index 000000000000..16aeb18d58e3 --- /dev/null +++ b/examples/postgres/composite_types/src/schema.rs @@ -0,0 +1,21 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + colors (color_id) { + color_id -> Int4, + red -> Int4, + green -> Int4, + blue -> Int4, + color_name -> Nullable, + } +} + +diesel::table! { + coordinates (coord_id) { + coord_id -> Int4, + xcoord -> Int4, + ycoord -> Int4, + } +} + +diesel::allow_tables_to_appear_in_same_query!(colors, coordinates,); From 2488ddaa963fc686afac805ecc6adfa5e1ce4eab Mon Sep 17 00:00:00 2001 From: "Peter C. S. Scholtens" Date: Fri, 17 Nov 2023 18:17:44 +0100 Subject: [PATCH 2/6] typo found by CI --- examples/postgres/composite_types/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/postgres/composite_types/README.md b/examples/postgres/composite_types/README.md index 043163e387fa..ffbc1f67ba96 100644 --- a/examples/postgres/composite_types/README.md +++ b/examples/postgres/composite_types/README.md @@ -129,7 +129,7 @@ On the Rust side, we define the interpretation of both functions differently, th sql_function!(fn color2grey(r: Integer, g: Integer,b: Integer) -> Record<(Float,Text)>); sql_function!(fn color2gray(r: Integer, g: Integer,b: Integer) -> PgGrayType); ``` -As this only creates a type with anonymous fields, which can be adressed by their field number **object.0**, **object.1** etc., it would be more convenient to attach names to the fields. Therefore we need to define a type with our intended field names, which we can use _globally_ (or at least outside the database related code space): +As this only creates a type with anonymous fields, which can be addressed by their field number **object.0**, **object.1** etc., it would be more convenient to attach names to the fields. Therefore we need to define a type with our intended field names, which we can use _globally_ (or at least outside the database related code space): ```rust #[derive(Debug, FromSqlRow)] pub struct GrayType { From 0e3d6d85252d7388e269ecca6871b627418a0f29 Mon Sep 17 00:00:00 2001 From: "Peter C. S. Scholtens" Date: Mon, 20 Nov 2023 09:44:53 +0100 Subject: [PATCH 3/6] Update README.md --- examples/postgres/composite_types/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/postgres/composite_types/README.md b/examples/postgres/composite_types/README.md index ffbc1f67ba96..52e6d3d1a8f1 100644 --- a/examples/postgres/composite_types/README.md +++ b/examples/postgres/composite_types/README.md @@ -141,7 +141,7 @@ The derived [FromSqlRow](https://docs.rs/diesel/latest/diesel/deserialize/trait. ```rust impl FromSql for GrayType { fn from_sql(bytes: PgValue) -> deserialize::Result { - let (intensity, suggestion) = FromSql::, Pg>::from_sql(bytes)?; + let (intensity, suggestion) = FromSql::::from_sql(bytes)?; Ok(GrayType { intensity, suggestion, From 2fa1dcf4716474f70c363f44af27ad822419ca34 Mon Sep 17 00:00:00 2001 From: "Peter C. S. Scholtens" Date: Mon, 20 Nov 2023 09:45:26 +0100 Subject: [PATCH 4/6] Update composite2rust_colors.rs --- .../postgres/composite_types/examples/composite2rust_colors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/postgres/composite_types/examples/composite2rust_colors.rs b/examples/postgres/composite_types/examples/composite2rust_colors.rs index c332396fc9f3..1c87b368966b 100644 --- a/examples/postgres/composite_types/examples/composite2rust_colors.rs +++ b/examples/postgres/composite_types/examples/composite2rust_colors.rs @@ -30,7 +30,7 @@ type PgGrayType = Record<(Float, Text)>; // Explain how this Postgres type can be converted to a Rust type. impl FromSql for GrayType { fn from_sql(bytes: PgValue) -> deserialize::Result { - let (intensity, suggestion) = FromSql::, Pg>::from_sql(bytes)?; + let (intensity, suggestion) = FromSql::::from_sql(bytes)?; Ok(GrayType { intensity, suggestion, From e71332c0b28a225c18e21549ab6e737f9879578a Mon Sep 17 00:00:00 2001 From: "Peter C. S. Scholtens" Date: Tue, 21 Nov 2023 15:24:17 +0100 Subject: [PATCH 5/6] Avoid deadlock with concurrent test The sql_query commands on lines 334 and 372 can be executes in parallel during a test and create a failure. With locking the users table this is avoided. --- diesel_tests/tests/update.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/diesel_tests/tests/update.rs b/diesel_tests/tests/update.rs index 238eb10f50a0..8c9f949e7a65 100644 --- a/diesel_tests/tests/update.rs +++ b/diesel_tests/tests/update.rs @@ -331,7 +331,8 @@ fn upsert_with_sql_literal_for_target() { let connection = &mut connection(); // This index needs to happen before the insert or we'll get a deadlock // with any transactions that are trying to get the row lock from insert - diesel::sql_query("CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'") + // As tests can run concurrently, lock the table of interest. + diesel::sql_query("BEGIN; LOCK TABLE users; CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'; COMMIT;") .execute(connection) .unwrap(); insert_sean_and_tess_into_users_table(connection); @@ -369,7 +370,8 @@ fn upsert_with_sql_literal_for_target_with_condition() { let connection = &mut connection(); // This index needs to happen before the insert or we'll get a deadlock // with any transactions that are trying to get the row lock from insert - diesel::sql_query("CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'") + // As tests can run concurrently, lock the table of interest. + diesel::sql_query("BEGIN; LOCK TABLE users; CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'; COMMIT;") .execute(connection) .unwrap(); insert_sean_and_tess_into_users_table(connection); From a1d03d2cb485e5f5b33db1a9d87c20277beb2466 Mon Sep 17 00:00:00 2001 From: "Peter C. S. Scholtens" Date: Wed, 22 Nov 2023 18:44:18 +0100 Subject: [PATCH 6/6] Update update.rs Remove multiple statements --- diesel_tests/tests/update.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/diesel_tests/tests/update.rs b/diesel_tests/tests/update.rs index 8c9f949e7a65..238eb10f50a0 100644 --- a/diesel_tests/tests/update.rs +++ b/diesel_tests/tests/update.rs @@ -331,8 +331,7 @@ fn upsert_with_sql_literal_for_target() { let connection = &mut connection(); // This index needs to happen before the insert or we'll get a deadlock // with any transactions that are trying to get the row lock from insert - // As tests can run concurrently, lock the table of interest. - diesel::sql_query("BEGIN; LOCK TABLE users; CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'; COMMIT;") + diesel::sql_query("CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'") .execute(connection) .unwrap(); insert_sean_and_tess_into_users_table(connection); @@ -370,8 +369,7 @@ fn upsert_with_sql_literal_for_target_with_condition() { let connection = &mut connection(); // This index needs to happen before the insert or we'll get a deadlock // with any transactions that are trying to get the row lock from insert - // As tests can run concurrently, lock the table of interest. - diesel::sql_query("BEGIN; LOCK TABLE users; CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'; COMMIT;") + diesel::sql_query("CREATE UNIQUE INDEX ON users (name) WHERE name != 'Tess'") .execute(connection) .unwrap(); insert_sean_and_tess_into_users_table(connection);