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:
flowchart LR
dialect[Dialect] --> ret[ReturningSupport]
dialect --> json[JsonSupport]
dialect --> arr[ArraySupport]
dialect --> upsert[UpsertSupport]
dialect --> idx[IndexIfNotExists]
| Trait | Operations Enabled |
|---|---|
ReturningSupport | insertReturning, updateReturning, deleteReturning |
JsonSupport | JSON query ops (.where(_.col).jsonContains/jsonHasKey/jsonPath), JSON type mappings |
ArraySupport | array containment queries, array type mappings |
UpsertSupport | Upsert DSL (Upsert[A].values(...).onConflict(_.col)) |
IndexIfNotExistsSupport | Conditional index creation |
Which shipped dialect has the trait:
| Database | Returning | JSON | Arrays | Upsert | Index IF NOT EXISTS |
|---|---|---|---|---|---|
| PostgreSQL | yes | yes | yes | yes | yes |
| MySQL | yes | ||||
| SQLite | yes | ||||
| H2 | |||||
| Spark | yes | yes |
On a database without the trait, use the portable form: insert instead of insertReturning, .inList instead of an array parameter, and a separate UPDATE instead of Upsert. The method will not compile against that dialect.
Using SpecializedDML
The SpecializedDML object provides type-safe operations that only compile when the dialect supports them:
(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
.provideLayer(DocsTransactor.layer)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 capabilityOperations 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[CapabilityItem].set(_.name, "x").where(_.id).eq(1).returningAs.build.sqlupdate "capabilities_pg_items" set "name" = $1 where "capabilities_pg_items"."id" = $2 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:
| Operation | Required Capability |
|---|---|
insertReturning | ReturningSupport |
updateReturning | ReturningSupport |
deleteReturning | ReturningSupport |
Upsert DSL (onConflict(_.col)) | UpsertSupport |
JSON query ops (.where(_.col).jsonContains/jsonHasKey/jsonPath) | JsonSupport |
| array containment queries | ArraySupport |
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" = $1 where "capabilities_returning_items"."id" = $2 returning *We've already seen this in action: insertReturning works because PostgreSQL provides ReturningSupport:
(for
_ <- ddl.createTable[SpecializedItem](ifNotExists = true)
returned <- dml.insertReturning(SpecializedItem(-1, "Capability Demo", "demo"))
yield returned).either
.provideLayer(DocsTransactor.layer)Right(SpecializedItem(3,Capability Demo,demo))