Skunk Documentation

repository·main·Indexed 23 days ago

https://github.com/typelevel/skunk

A 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.

Tokens
21.7K
Snippets
52
Records
100
Agent score
81%

What's inside Skunk

  1. Understand PostgreSQL transaction statuses in Skunk

    main

    A Skunk Session is always in one of three transaction states. You can monitor this state via the session.transactionStatus member, which is an fs2 Signal.

    StatusDescription
    IdleNo transaction is currently in progress.
    ActiveA transaction is in progress and can proceed.
    ErrorAn error has occurred. The transaction must be rolled back to a savepoint to continue, or rolled back entirely to terminate.
  2. Understand Twiddle Lists for Query and Command parameters

    main

    Twiddle lists are tuples built incrementally using the *: operator. In Skunk, they are used as type arguments for Query and Command to represent parameter encoders and row decoders.

    Scala 3 Behavior: *: and EmptyTuple are 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 *: and EmptyTuple as aliases for Shapeless HLists. 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)
  3. Control transactions with the advanced usage pattern

    main

    For more granular control, use the transaction reference xa provided by the use block. This allows you to manage savepoints, explicit commits, and rollbacks.

    Available actions on xa:

    • xa.status: Yields the current TransactionStatus (as an fs2 Signal).
    • xa.commit: Commits the transaction explicitly.
    • xa.rollback: Rolls back the entire transaction.
    • xa.savepoint: Creates a new xa.Savepoint.
    • xa.rollback(sp): Rolls back to a specific xa.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:

    StatusNormal ExitCancellationError Raised
    Idlere-raise
    Activecommitroll backroll back, re-raise
    Errorroll backroll backroll back, re-raise

    Note: An Idle status on normal exit means the user terminated the transaction explicitly inside the block.

  4. Use AppliedFragments for dynamic query construction

    main

    An AppliedFragment allows 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. AppliedFragment forms a monoid, allowing you to combine them using |+| or foldSmash.

    To execute an AppliedFragment, you must extract its underlying Query and its argument (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))
    }
  5. Choosing between Simple and Extended query protocols

    main

    Skunk 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).
  6. Compose multiple values with Composite Encoders

    main

    You can combine two encoders a: Encoder[A] and b: Encoder[B] into a composite encoder a ~ b of type Encoder[(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)"
  7. Use Postgres Arrays with skunk.data.Arr

    main

    Postgres arrays are represented in Skunk by skunk.data.Arr. Note that Skunk arrays are rectangular (unlike Scala Arrays, 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 Arr API is not finalized and may change.

    Numeric Array Types

    Postgres TypeScala Type
    _int2Arr[Short]
    _int4Arr[Int]
    _int8Arr[Long]
    _numericArr[BigDecimal]
    _float4Arr[Float]
    _float8Arr[Double]

    Character Array Types

    Postgres TypeScala TypeNotes
    _varcharArr[String]Length argument not yet supported
    _bpcharArr[String]Length argument not yet supported
    _textArr[String]

    Note: For _int8 and 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]`    |