Getting Started

Installation

Add Saferis to your build.sbt:

libraryDependencies ++= Seq(
  "rocks.earlyeffect" %% "saferis" % "0.20.0",
  "rocks.earlyeffect" %% "saferis-postgres-jdbc" % "0.20.0",
)

Add saferis and the one adapter for your database. saferis-jdbc is java.sql only, and each adapter supplies what its database does differently (bind, read, server errors, cursors) and brings its own JDBC driver:

DatabaseAdapter moduleLayer
PostgreSQLsaferis-postgres-jdbc (pgjdbc)PostgresJdbc.layer()
MySQLsaferis-mysql-jdbc (Connector/J)MySqlJdbc.layer() with import saferis.mysql.given
SQLitesaferis-sqlite-jdbc (sqlite-jdbc)SqliteJdbc.layer() with import saferis.sqlite.given
H2, for fast in-memory testssaferis-h2-jdbcH2Jdbc.layer() with import saferis.h2.given

A database Saferis does not ship needs a Dialect and a JdbcAdapter. Extend StandardJdbcAdapter, override what your driver does differently, and pass it to JdbcSession.layer.

A Node application depends on the Scala.js artifacts and on npm packages. The versions below are the ones this repository tests. A missing require fails when the bundle loads.

libraryDependencies ++= Seq(
  "rocks.earlyeffect" %%% "saferis" % "0.20.0",
  "rocks.earlyeffect" %%% "saferis-postgres-node" % "0.20.0",
)
npm install pg@8.16.3 pg-cursor@2.22.0

Saferis requires ZIO as a provided dependency:

libraryDependencies += "dev.zio" %% "zio" % "2.1.24"

Quick Example

Saferis operations are plain ZIO effects. Throughout these docs the examples are real ZIO programs run against a live PostgreSQL database. The suite provides an SqlSession connected to a test container.

Define a table with the Table typeclass, then create it, insert rows, and query it, all type-safe, all against a real database:

(for
  _     <- ddl.createTable[QuickUser](ifNotExists = true)
  _     <- dml.insert(QuickUser(-1, "Alice", "alice@example.com"))
  _     <- dml.insert(QuickUser(-1, "Bob", "bob@example.com"))
  users <- sql"SELECT * FROM ${Table[QuickUser]}".query[QuickUser]
yield users).either
  .provideLayer(DocsTransactor.layer)
Right(Chunk(QuickUser(1,Alice,alice@example.com),QuickUser(2,Bob,bob@example.com)))

Anatomy of an Application

In a real application you provide SqlSession as a layer and let your ZIOAppDefault run the program. Fragments read the session from the environment:

flowchart TB
  ds[DataSource] --> session[PostgresJdbc.layer]
  session --> prog[ZIO program]
import saferis.*
import saferis.postgres.jdbc.PostgresJdbc
import zio.*
import javax.sql.DataSource

@tableName("getting_started_app_users")
case class AppUser(@generated @key id: Int, name: String) derives Table

object MyApp extends ZIOAppDefault:
  val program: ZIO[SqlSession, SaferisError, Chunk[AppUser]] =
    for
      _     <- ddl.createTable[AppUser](ifNotExists = true)
      _     <- dml.insert(AppUser(-1, "Alice"))
      users <- sql"SELECT * FROM ${Table[AppUser]}".query[AppUser]
    yield users

  // A JDBC pool (for example HikariCP) is the DataSource.
  // PostgresJdbc.layer is the Postgres JdbcAdapter on that pool.
  def dataSource: DataSource = ???

  def run = program.provide(ZLayer.succeed(dataSource) >>> PostgresJdbc.layer())

Next, read Core Concepts to understand table definitions, the sql"..." interpolator, and SqlSession.