Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions docs/how-to/configure-the-database.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,30 @@ The pool starts empty and grows to the size as connections are needed.
## One database, several applications

Each application can keep its tables in a Postgres schema of its own, so two applications can
share the dev database without their drift bleeding into each other. Two overrides on `Main`,
told once each:
share the dev database without their drift bleeding into each other. Name the schema on `Main`:

```scala
object Main extends EezoApp {

override def databaseSchema: String = "bookshelf"
// ...
}
```

That one override is enough. Every connection the pool opens creates the schema when it is
missing and sets `search_path` to it, because `search_path` is per connection, so the tables
`sync` creates, the migration ledger and your own queries all land in `bookshelf`. The schema
commands read the catalog under the same name, so `status` compares your model against the right
tables. The examples under `examples/` each do this.

The schema is created only when the catalog says it is missing, so a production role that owns an
existing schema but may not create one on the database still connects. `search_path` is the schema
alone: objects in `public`, such as an extension's types and functions, are not on it. An
application that needs those too sets the path itself in `databaseInit`, which runs after the
schema step: `set search_path to bookshelf, public`.

`databaseInit` is for any other per connection setting, such as `application_name` or a statement
timeout. It runs after the schema step on every pooled connection and needs no `super` call:

```scala
import java.sql.Connection
Expand All @@ -58,20 +80,13 @@ object Main extends EezoApp {

override def databaseInit: Connection => Unit = { c =>
val st = c.createStatement()
try {
st.execute("""create schema if not exists "bookshelf"""")
st.execute("""set search_path to "bookshelf"""")
} finally st.close()
try st.execute("set application_name to 'bookshelf'")
finally st.close()
}
// ...
}
```

`databaseInit` runs on every pooled connection, because `search_path` is per connection.
`databaseSchema` points the schema commands' catalog reads at the same name, so `status` compares
your model against the right tables. Both halves are needed; the examples under `examples/` each
do this.

## In tests

Point a second `DbApp` at a throwaway Postgres, such as one from testcontainers, by overriding
Expand Down
15 changes: 9 additions & 6 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,22 @@ overridden `boot` that still wants to serve calls it.
|---|---|---|
| `schema: Schema` | abstract | per command |
| `boot(): Unit` | abstract on `DbApp`; `EezoApp` supplies the http layer's | the program |
| `databaseSchema: String` | `"public"` | by the commands that introspect |
| `databaseSchema: String` | `"public"` | on every physical connection the pool opens, and by the commands that introspect |
| `databaseUrl: String` | from the environment, above | when a `Database` is built |
| `databaseUser: String` | from the environment | same |
| `databasePassword: String` | from the environment | same |
| `databasePoolSize: Int` | from the environment, 10 | same |
| `databaseAcquireTimeout: Duration` | from the environment, 5 s | same |
| `databaseInit: Connection -> Unit` | nothing | on every physical connection the pool opens |
| `databaseInit: Connection -> Unit` | nothing | on every physical connection the pool opens, after the schema step |

A `Database` is built per command and per program run, installed around it, and closed after.
`databaseInit` is where `search_path`, `application_name` and statement timeouts go. An
application that keeps its tables in a schema other than `public` sets both `databaseSchema`
and a `databaseInit` that sets `search_path`, because the first is what the drift commands
read and the second is what every query runs under.
`databaseSchema` is where the application's tables live and what the drift commands read. Any
name but `public` makes every physical connection create that schema when the catalog says it is
missing and set `search_path` to it alone, so the DDL, the migration ledger and every query land
where `status` looks; `public` runs nothing and leaves Postgres's own `search_path`. The name is
quoted as an identifier, so it is used exactly as written. `databaseInit` is where
`application_name`, statement timeouts and any other per connection setting go; it runs after the
schema step and needs no `super` call to keep it.

## `LiveApp`

Expand Down
4 changes: 2 additions & 2 deletions examples/blog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ where they came from and wraps only the derived ones in `Route.under("/admin")`,
routes and the URLs the pages emit together; `app/Index.scala` explains the one link that has to
name `/admin` by hand.

The blog keeps its table in a Postgres schema named `blog` (`databaseSchema` plus the `databaseInit`
hook), so it shares the dev database with the other examples.
The blog keeps its table in a Postgres schema named `blog` (`databaseSchema` in `Main.scala`, the
one override that does it), so it shares the dev database with the other examples.

```bash
docker run -d --name eezo-pg -p 5442:5432 \
Expand Down
10 changes: 0 additions & 10 deletions examples/blog/src/main/scala/CreateUser.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import java.sql.Connection

import io.eezo.auth.Password
import io.eezo.core.Id
import io.eezo.db.*
Expand Down Expand Up @@ -34,14 +32,6 @@ object CreateUser extends DbApp {

override def databaseSchema: String = "blog"

override def databaseInit: Connection => Unit = { c =>
val st = c.createStatement()
try {
st.execute("""create schema if not exists "blog"""")
st.execute("""set search_path to "blog"""")
} finally st.close()
}

override def boot(): Unit = {
val email = required("EEZO_BLOG_EMAIL")
val password = required(PasswordVariable)
Expand Down
16 changes: 3 additions & 13 deletions examples/blog/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import java.sql.Connection

import io.eezo.EezoApp
import io.eezo.db.Schema
import io.eezo.generated.Routes
Expand Down Expand Up @@ -45,20 +43,12 @@ object Main extends EezoApp {
override def schema: Schema = AppSchema

/** This app keeps its table in a Postgres schema of its own, so it can share the dev database
* with the other examples without their drift bleeding into each other. Two halves, told once
* each: the init hook runs on **every** pooled connection (search_path is per connection), and
* `databaseSchema` points the drift commands' catalog reads at the same name.
* with the other examples without their drift bleeding into each other. Naming it is enough:
* eezo creates the schema and sets search_path to it on **every** pooled connection, because
* search_path is per connection, and the drift commands read the same name.
*/
override def databaseSchema: String = "blog"

override def databaseInit: Connection => Unit = { c =>
val st = c.createStatement()
try {
st.execute("""create schema if not exists "blog"""")
st.execute("""set search_path to "blog"""")
} finally st.close()
}

override def routes: RouteTable = {
val table = Routes.table()
val (derived, handwritten) = table.routes.partition(_.provenance == Provenance.Derived)
Expand Down
18 changes: 8 additions & 10 deletions examples/blog/src/test/scala/PostOwnershipSuite.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.sql.Connection
import java.util.UUID

import io.eezo.core.Id
Expand Down Expand Up @@ -94,17 +93,16 @@ class PostOwnershipSuite extends munit.FunSuite {
*
* A second `DbApp` for the same reason `CreateUser` is one: `withDatabase` is `protected`, so
* installing a `Database` is something only a subclass can ask for, and a test is a front end
* like any other. The schema, the per connection hook and `AppSchema` are `Main`'s own values
* rather than copies, so a change to where the blog keeps its tables moves this with it.
* like any other. The schema and `AppSchema` are `Main`'s own values rather than copies, so a
* change to where the blog keeps its tables moves this with it.
*/
private object BlogDb extends DbApp {
override def schema: Schema = Main.schema
override def databaseUrl: String = postgres.getJdbcUrl
override def databaseUser: String = postgres.getUsername
override def databasePassword: String = postgres.getPassword
override def databaseSchema: String = Main.databaseSchema
override def databaseInit: Connection => Unit = Main.databaseInit
override def boot(): Unit = ()
override def schema: Schema = Main.schema
override def databaseUrl: String = postgres.getJdbcUrl
override def databaseUser: String = postgres.getUsername
override def databasePassword: String = postgres.getPassword
override def databaseSchema: String = Main.databaseSchema
override def boot(): Unit = ()

/** The one thing this object exists to publish: a `Database` installed around a block, which is
* what `sbt run` does around `boot` and what every handler below reaches through.
Expand Down
6 changes: 3 additions & 3 deletions examples/reminders/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ deliver every reminder that is due and not yet sent, and mark it sent, in one `t
is a line on stdout. `DbApp` brings `main`, the `Database` it installs around `boot`, and the eight
schema commands. There is no `EezoPlugin` on this project, because there are no routes to generate.

The job keeps its table in a Postgres schema named `reminders` (`databaseSchema` plus the
`databaseInit` hook; `Main.scala` says why both halves exist), so it shares the dev database with
the other examples without their drift bleeding into each other.
The job keeps its table in a Postgres schema named `reminders` (`databaseSchema` in `Main.scala`,
the one override that does it), so it shares the dev database with the other examples without
their drift bleeding into each other.

```bash
docker run -d --name eezo-pg -p 5442:5432 \
Expand Down
15 changes: 3 additions & 12 deletions examples/reminders/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import java.sql.Connection
import java.time.LocalDate

import io.eezo.db.*
Expand All @@ -25,20 +24,12 @@ object Main extends DbApp {
override def schema: Schema = AppSchema

/** This app keeps its table in a Postgres schema of its own, so it can share the dev database
* with the other examples without their drift bleeding into each other. Two halves, told once
* each: the init hook runs on **every** pooled connection (search_path is per connection), and
* `databaseSchema` points the drift commands' catalog reads at the same name.
* with the other examples without their drift bleeding into each other. Naming it is enough:
* eezo creates the schema and sets search_path to it on **every** pooled connection, because
* search_path is per connection, and the drift commands read the same name.
*/
override def databaseSchema: String = "reminders"

override def databaseInit: Connection => Unit = { c =>
val st = c.createStatement()
try {
st.execute("""create schema if not exists "reminders"""")
st.execute("""set search_path to "reminders"""")
} finally st.close()
}

override def boot(): Unit = {
val reminders = Table[Reminder]
val today = LocalDate.now()
Expand Down
10 changes: 0 additions & 10 deletions examples/shop/src/main/scala/CreateUser.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import java.sql.Connection

import io.eezo.auth.Password
import io.eezo.core.Id
import io.eezo.db.*
Expand All @@ -15,14 +13,6 @@ object CreateUser extends DbApp {
/** Same Postgres schema as `Main`, so the account lands where the app reads it. */
override def databaseSchema: String = "shop"

override def databaseInit: Connection => Unit = { c =>
val st = c.createStatement()
try {
st.execute("""create schema if not exists "shop"""")
st.execute("""set search_path to "shop"""")
} finally st.close()
}

override def boot(): Unit = {
val email = sys.env("SHOP_EMAIL")
val hashed = Field[Password]
Expand Down
16 changes: 3 additions & 13 deletions examples/shop/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import java.sql.Connection

import io.eezo.EezoApp
import io.eezo.db.Schema
import io.eezo.generated.Routes
Expand All @@ -12,17 +10,9 @@ object Main extends EezoApp {
override def layout: Layout = views.Layout

/** The shop keeps its tables in a Postgres schema of its own, so it shares the dev database with
* the other examples without their drift bleeding into each other. The init hook runs on every
* pooled connection, because search_path is per connection, and `databaseSchema` points the
* drift commands at the same name.
* the other examples without their drift bleeding into each other. Naming it is enough: eezo
* creates the schema and sets search_path to it on every pooled connection, and the drift
* commands read the same name.
*/
override def databaseSchema: String = "shop"

override def databaseInit: Connection => Unit = { c =>
val st = c.createStatement()
try {
st.execute("""create schema if not exists "shop"""")
st.execute("""set search_path to "shop"""")
} finally st.close()
}
}
19 changes: 8 additions & 11 deletions examples/shop/src/test/scala/ApiSuite.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.sql.Connection

import io.eezo.auth.{Password, Token}
import io.eezo.core.Id
Expand Down Expand Up @@ -49,18 +48,16 @@ class ApiSuite extends munit.FunSuite {
started
}

/** The shop's own database settings pointed at the container. `Main`'s schema name and per
* connection hook rather than copies, so the routes read the `shop` search path they read in
* production.
/** The shop's own database settings pointed at the container. `Main`'s schema name rather than a
* copy, so the routes read the `shop` search path they read in production.
*/
private object ShopDb extends DbApp {
override def schema: Schema = Main.schema
override def databaseUrl: String = postgres.getJdbcUrl
override def databaseUser: String = postgres.getUsername
override def databasePassword: String = postgres.getPassword
override def databaseSchema: String = Main.databaseSchema
override def databaseInit: Connection => Unit = Main.databaseInit
override def boot(): Unit = ()
override def schema: Schema = Main.schema
override def databaseUrl: String = postgres.getJdbcUrl
override def databaseUser: String = postgres.getUsername
override def databasePassword: String = postgres.getPassword
override def databaseSchema: String = Main.databaseSchema
override def boot(): Unit = ()

def around[A](body: => A): A = withDatabase(body)
}
Expand Down
Loading
Loading