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:
| 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 |
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))
.eitherRight((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[PgItem].set(_.name, "x").where(_.id).eq(1).returningAs.build.sqlupdate 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:
| 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 = ? 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)
.eitherRight(SpecializedItem(3,Capability Demo,demo))