Skip to content

Commit

Permalink
save docs
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Oct 16, 2024
1 parent 5086a04 commit b076802
Show file tree
Hide file tree
Showing 13 changed files with 972 additions and 1 deletion.
8 changes: 7 additions & 1 deletion docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ async fn main() -> Result<(), http::Error> {
Rwf uses the `log` crate for logging. `Logger::init()` automatically configures it for your app using `env_logger`, but if you prefer, you can configure logging yourself
using the crate of your choosing.

Once the server is launched, you can visit the index page by pointing your browser to [http://localhost:8000](http://localhost:8000).
Launching the server can be done with Cargo:

```
cargo run
```

Once the server is running, you can visit the index page by pointing your browser to [http://localhost:8000](http://localhost:8000).

Full code for this is available in GitHub in [examples/quick-start](https://github.com/levkk/rwf/tree/main/examples/quick-start).
157 changes: 157 additions & 0 deletions docs/docs/models/create-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Create records

Rwf can create model records in one of two ways:

- `Model::save` method which is called on an instance of a struct implementing the `Model` trait
- `Model::create` method which accepts the column names and their respective values as input

## Saving models

Using our `User` model from our [previous example](../), we can create a new record by instantiating a new instance of the `User` struct and calling `save`:

```rust
let user = User {
id: None,
email: "[email protected]".to_string(),
created_at: OffsetDateTime::now_utc(),
};

let user = user
.save()
.fetch(&mut conn)
.await?;
```

!!! note
The `id` field is set to `None`. This ensures that the database
assigns it a value automatically, and that this value is unique.

Calling `save` on a model struct with the `id` set to `None` produces the following query:

```postgresql
INSERT INTO "users" ("email", "created_at") VALUES ($1, $2) RETURNING *
```

## Using table defaults

If you don't want to specify some columns when creating records and your database schema has configured defaults, you can use the `Model::create`
method instead:

=== "Rust"

```rust
let user = User::create(&[
("email", "[email protected]"),
])
.fetch(&mut conn)
.await?
```

=== "SQL"

```postgresql
INSERT INTO "users" ("email") VALUES ($1) RETURNING *
```

Any columns not specified in the `INSERT` statement will be automatically filled in with column defaults. For example, the `created_at` column
specified in our [previous example](../) has a default value `NOW()`, the current database time.

## Unique constraints

It's very common to place unique constraints on certain columns in a table to avoid duplicate records. For example, the `"users"` table
would typically have a unique contraint on the `email` column, ensuring that no two users have the same email address.

To handle unique contraints, Rwf can update a record in-place if one exists already matching the constraint:

=== "Rust"
```rust
let user = User::create(&[
("email", "[email protected]")
])
.unique_by(&["email"])
.fetch(&mut conn)
.await?;
```
=== "SQL"
```postgresql
INSERT INTO "users" ("email") VALUES ($1)
ON CONFLICT ("email") DO UPDATE
SET "email" = EXCLUDED."email"
RETURNING *
```

## Optionally create records

If the record matching the `INSERT` statement exists already, Rwf supports returning the exisitng row without performing an update:

=== "Rust"
```rust
let user = User::find_or_create_by(&[
("email", "[email protected]")
])
.fetch(&mut conn)
.await?;
```
=== "SQL"
This executes _up to_ two queries, starting with:

```postgresql
SELECT * FROM "users" WHERE "email" = $1
```

If a row is returned, no more queries are executed. However, if no rows matching the condition exist,
an `INSERT` query is executed:

```postgresql
INSERT INTO "users" ("email") VALUES ($1) RETURNING *
```

### Combining with a unique constraint

In busy web apps which execute thousands of queries per second, it's entirely possible for a record to be created between the time the `SELECT` query
returns no rows and an `INSERT` query is sent to the database. In this case, a unique constraint violation error will be returned. To avoid this,
it's possible to combine `unque_by` with `find_or_create_by` executed inside a single transaction:

=== "Rust"
```rust
// Start a transaction explicitely.
let transaction = Pool::transaction().await?;

let user = User::find_or_create_by(&[
("email", "[email protected]")
])
.unique_by(&["email"])
.fetch(&mut transaction)
.await?;

// Commit the transaction.
transaction.commit().await?;
```
=== "SQL"
A transaction is started explicitely:
```postgresql
BEGIN;
```

Afterwards, the ORM attempts to find a record matching the columns
in the `INSERT` statement:

```postgresql
SELECT * FROM "users" WHERE "email" = $1
```

If this query returns a row, no more queries are executed. Otherwise,
an `INSERT` query with `ON CONFLICT` clause is sent to the database:

```postgresql
INSERT INTO "users" ("email") VALUES ($1)
ON CONFLICT ("email") DO UPDATE
SET "email" = EXCLUDED."email"
RETURNING *
```

Finally, the transaction is committed to the database:

```postgresql
COMMIT;
```
43 changes: 43 additions & 0 deletions docs/docs/models/custom-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Custom queries

Sometimes the ORM is not enough and you need to write a complex query by hand. Rwf provides an easy way to execute arbitrary queries
and map the results to a model struct:

```rust
let users = User::find_by_sql("SELECT * FROM users ORDER BY RANDOM() LIMIT 1")
.fetch_all(&mut conn)
.await?;
```

!!! note
Since this query is not generated by the ORM, you need to make sure
to return all the necessary columns and correct data types to map the results to the Rust struct.


## Use the database driver directly

If you want to bypass the ORM entirely and just execute queries, you can do so by checking out a connection and calling the `query_cached` method on it:

```rust
let mut conn = Pool::connection().await?;

let results = conn
.query_cached("SELECT * FROM wherever WHERE column = $1", &[5])
.await?;
```

Rwf uses `tokio_postgres` underneath to talk to Postgres, so you'll receive a `Vec<tokio_postgres::Row>` as a result of executing that function.

Since `tokio_postgres` uses prepared statements, `query_cached` ensures that identical queries are not prepared more than once per connection. If you want
to bypass that and use `tokio_postgres` directly, you can use `client()` instead:

```rust
let mut conn = Pool::connection().await?;

// Returns a `tokio_postgres::Client`
let client = conn.client();

client
.execute("SELECT 1", &[])
.await?;
```
42 changes: 42 additions & 0 deletions docs/docs/models/debug-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Debug queries

When building queries using the ORM, the end result can be inspected by calling `to_sql()`:

=== "Rust"
```rust
let query = User::all()
.filter("created_at", Value::Null)
.limit(5)
.to_sql();
println!("Query: {}", query);
```
=== "Output"
```
Query: SELECT * FROM "users" WHERE "created_at" IS NULL LIMIT 5
```

The query will not be sent to the database, so it's safe to inspect all queries, no matter if they are performant or not.

## Query plan

Visual inspection of the query is often not sufficient to undertand query performance. For this purpose, databases like PostgreSQL provide
the `EXPLAIN` functionality which, instead of executing the query, produces an execution plan:

=== "Rust"
```rust
let plan = User::find(15)
.explain(&mut conn)
.await?;
println!("{}", plan);
```
=== "SQL"
```postgresql
EXPLAIN SELECT * FROM "users" WHERE "id" = $1
```
=== "Output"
```
Seq Scan on users (cost=0.00..25.00 rows=6 width=40)
Filter: (id = 5)
```

When optimizing queries, this functionality is useful for finding queries that should be using indexes but perform a sequential scan instead.
Loading

0 comments on commit b076802

Please sign in to comment.