Skunk Documentation
repository·main·Indexed 23 days ago
https://github.com/typelevel/skunkA type-safe data access library specifically designed for Scala and PostgreSQL. It provides tools for interacting with Postgres databases, including Encoder and Decoder typeclasses for transforming Scala types to Postgres text-format data, a Channel API for Pub/Sub using LISTEN and NOTIFY, and SqlState extractors for handling PostgresErrorException.
What's inside Skunk
- Skunk is a data access library designed for Scala and PostgreSQL. It provides a way to interact with Postgres databases within the Scala ecosystem.
What is a Command in Skunk?
mainA Command is a SQL statement that does not return rows (e.g.,
INSERT,UPDATE,DELETE, orSET).Commands are parameterized by their input type. A command with no parameters has the input type
Void.val a: Command[Void] = sql"SET SEED TO 0.123".commandChoosing between Simple and Extended Command Protocols
mainWhen deciding how to execute a command, consider the following:
Protocol Method Use when... Simple Session#executeCommand has no parameters AND you only use it once per session. Extended Session#prepareCommand has parameters OR you intend to reuse the command multiple times per session. Understand PostgreSQL transaction statuses in Skunk
mainA Skunk
Sessionis always in one of three transaction states. You can monitor this state via thesession.transactionStatusmember, which is an fs2Signal.Status Description Idle No transaction is currently in progress. Active A transaction is in progress and can proceed. Error An error has occurred. The transaction must be rolled back to a savepoint to continue, or rolled back entirely to terminate. Understand Twiddle Lists for Query and Command parameters
mainTwiddle lists are tuples built incrementally using the
*:operator. In Skunk, they are used as type arguments forQueryandCommandto represent parameter encoders and row decoders.Scala 3 Behavior:
*:andEmptyTupleare part of the standard library and behave exactly like n-argument tuples. You can use standard tuple syntax or the twiddle syntax.Scala 2 Behavior: Skunk uses the Typelevel Twiddles library, which polyfills
*:andEmptyTupleas aliases for ShapelessHLists. Twiddles provides implicit conversions between twiddle lists and regular tuples.// Example of a Query using Twiddle Lists for parameter types val q: Query[Short *: String *: String *: Int *: EmptyTuple] = sql""" SELECT name, age FROM person WHERE age < $int2 AND status = $ """ .query(varchar *: int4)Handle nullable columns in Skunk queries
mainWhen working with columns that can contain
NULLvalues in PostgreSQL, you must use the.optmodifier on your encoders and decoders.- For standard decoders/encoders: Use
int4.optinstead ofint4. - For interpolated SQL (using
sql"..."): Use the syntax${int4.opt}within the query string.
- For standard decoders/encoders: Use
Control transactions with the advanced usage pattern
mainFor more granular control, use the transaction reference
xaprovided by theuseblock. This allows you to manage savepoints, explicit commits, and rollbacks.Available actions on
xa:xa.status: Yields the currentTransactionStatus(as an fs2Signal).xa.commit: Commits the transaction explicitly.xa.rollback: Rolls back the entire transaction.xa.savepoint: Creates a newxa.Savepoint.xa.rollback(sp): Rolls back to a specificxa.Savepoint, allowing the transaction to continue after an error.
Transaction Finalization Logic
When using the advanced pattern, Skunk determines whether to commit or rollback based on the session's status and how the block exited:
Status Normal Exit Cancellation Error Raised Idle — — re-raise Active commit roll back roll back, re-raise Error roll back roll back roll back, re-raise Note: An Idle status on normal exit means the user terminated the transaction explicitly inside the block.
Use AppliedFragments for dynamic query construction
mainAn
AppliedFragmentallows you to bind a fragment to a set of arguments at runtime. This is useful for building complex queries with optional filters on the fly.AppliedFragmentforms a monoid, allowing you to combine them using|+|orfoldSmash.To execute an
AppliedFragment, you must extract its underlyingQueryand itsargument(the bound values).Example: Dynamic Query Construction
def countryQuery(name: Option[String], pop: Option[Int]): AppliedFragment = { val base = sql"SELECT code FROM country" val nameLike = sql"name LIKE $varchar" val popGreaterThan = sql"population > $int4" val conds: List[AppliedFragment] = List( name.map(nameLike), pop .map(popGreaterThan), ).flatten val filter = if (conds.isEmpty) AppliedFragment.empty else conds.foldSmash(void" WHERE ", void" AND ", AppliedFragment.empty) base(Void) |+| filter } // To execute: def usage(s: Session[IO]) = { val f = countryQuery(Some("Un%"), None) // Returns AppliedFragment val q = f.fragment.query(varchar) // Extract Query[f.A, String] s.prepare(q).flatMap(_.stream(f.argument, 64).compile.to(List)) }def countryQuery(name: Option[String], pop: Option[Int]): AppliedFragment = { val base = sql"SELECT code FROM country" val nameLike = sql"name LIKE $varchar" val popGreaterThan = sql"population > $int4" val conds: List[AppliedFragment] = List( name.map(nameLike), pop .map(popGreaterThan), ).flatten val filter = if (conds.isEmpty) AppliedFragment.empty else conds.foldSmash(void" WHERE ", void" AND ", AppliedFragment.empty) base(Void) |+| filter } // To execute: def usage(s: Session[IO]) = { val f = countryQuery(Some("Un%"), None) // Returns AppliedFragment val q = f.fragment.query(varchar) // Extract Query[f.A, String] s.prepare(q).flatMap(_.stream(f.argument, 64).compile.to(List)) }Choosing between Simple and Extended query protocols
mainSkunk supports two Postgres protocols. Choosing the right one depends on your use case:
Use the Simple Query Protocol (
Session#execute) if:- Your query has no parameters.
- You are querying for a small number of rows.
- You will use the query only once per session.
Use the Extended Query Protocol (
Session#prepare) if:- Your query has parameters.
- You are querying for a large or unknown number of rows.
- You intend to stream the results.
- You intend to use the query multiple times per session (prepared statements can be reused).
Compose multiple values with Composite Encoders
mainYou can combine two encoders
a: Encoder[A]andb: Encoder[B]into a composite encodera ~ bof typeEncoder[(A, B)]. This composite encoder expands to a sequence of placeholders in the resulting SQL.Composite structures are flattened in the final SQL statement, providing convenience on the Scala side while maintaining correct placeholder sequencing.
// Constructing a composite encoder from two base encoders sql"INSERT INTO person (name, age) VALUES (${varchar ~ int4})" // Flattening a composite structure val enc = varchar ~ int4 ~ float4 sql"INSERT INTO person (comment, name, age, weight, comment) VALUES ($text, $enc)"Use Postgres Arrays with skunk.data.Arr
mainPostgres arrays are represented in Skunk by
skunk.data.Arr. Note that Skunk arrays are rectangular (unlike ScalaArrays, which are ragged).Warning: Skunk does not yet support arrays containing nullable elements. Attempting to decode such a value will cause a runtime failure. The
ArrAPI is not finalized and may change.Numeric Array Types
Postgres Type Scala Type _int2Arr[Short]_int4Arr[Int]_int8Arr[Long]_numericArr[BigDecimal]_float4Arr[Float]_float8Arr[Double]Character Array Types
Postgres Type Scala Type Notes _varcharArr[String]Length argument not yet supported _bpcharArr[String]Length argument not yet supported _textArr[String]Note: For
_int8and character types, precision and length arguments are not yet supported.| Postgres Type | Scala Type | |-----------------|------------------| | `_int2` | `Arr[Short]` | | `_int4` | `Arr[Int]` | | `_int8` | `Arr[Long]` | | `_numeric` | `Arr[BigDecimal]` | | `_float4` | `Arr[Float]` | | `_float8` | `Arr[Double]` |Understand Skunk metrics
mainSkunk uses OpenTelemetry via theotel4simplementation. It provides thedb.client.operation.durationhistogram, which records the duration of all operations interacting with the PostgreSQL server.