Type-Safe Capabilities

Saferis uses Scala 3's type system to ensure operations are only available when the database supports them.

Capability Traits

Each dialect mixes in capability traits that enable specific operations:

TraitOperations Enabled
ReturningSupportinsertReturning, updateReturning, deleteReturning
JsonSupportJSON query ops (.where(_.col).jsonContains/jsonHasKey/jsonPath), JSON type mappings
ArraySupportarray containment queries, array type mappings
UpsertSupportUpsert DSL (Upsert[A].values(...).onConflict(_.col))
IndexIfNotExistsSupportConditional index creation

Using SpecializedDML

The SpecializedDML object provides type-safe operations that only compile when the dialect supports them:

xa.run(for
  _        <- ddl.createTable[SpecializedItem](ifNotExists = true)
  inserted <- dml.insertReturning(SpecializedItem(-1, "Widget", "hardware"))
  _        <- dml.insert(SpecializedItem(-1, "Gadget", "electronics"))
  all      <- sql"SELECT * FROM ${Table[SpecializedItem]}".query[SpecializedItem]
yield (inserted, all))
  .either
Right((SpecializedItem(1,Widget,hardware),Chunk(SpecializedItem(1,Widget,hardware),SpecializedItem(2,Gadget,electronics))))

Compile-Time Safety

Capabilities are encoded in the dialect's type. You can ask the compiler to prove a dialect supports a capability with a type ascription on summon[Dialect]. Each line below only compiles because PostgreSQL actually mixes in that capability:

"PostgreSQL provides every documented capability"
PostgreSQL provides every documented capability

Operations that require a capability take a using Dialect & SomeSupport parameter. Because the default dialect (PostgreSQL) provides every capability, returningAs compiles out of the box:

Update[PgItem].set(_.name, "x").where(_.id).eq(1).returningAs.build.sql
update capabilities_pg_items set name = ? where capabilities_pg_items.id = ? returning *

Switch to a dialect that lacks a capability, for example a SQLite-only program that tries an Upsert (SQLite has no UpsertSupport), and the operation no longer typechecks. The constraint is part of the method signature, so the mismatch is caught at compile time rather than failing against the database at runtime.

Available capability-constrained operations in SpecializedDML:

OperationRequired Capability
insertReturningReturningSupport
updateReturningReturningSupport
deleteReturningReturningSupport
Upsert DSL (onConflict(_.col))UpsertSupport
JSON query ops (.where(_.col).jsonContains/jsonHasKey/jsonPath)JsonSupport
array containment queriesArraySupport

Generic Functions with Capability Constraints

Write functions that require specific capabilities via using constraints. The constraint propagates to every caller, so a function that needs RETURNING can only be called where the dialect provides it:

updateAndReturn("x")
update capabilities_returning_items set name = ? where capabilities_returning_items.id = ? returning *

We've already seen this in action: insertReturning works because PostgreSQL provides ReturningSupport:

xa.run(for
  _        <- ddl.createTable[SpecializedItem](ifNotExists = true)
  returned <- dml.insertReturning(SpecializedItem(-1, "Capability Demo", "demo"))
yield returned)
  .either
Right(SpecializedItem(3,Capability Demo,demo))