Skip to content

flipstone/orville

Repository files navigation

Orville - a Haskell library for PostgreSQL

Orville's goal is to provide a powerful API for applications to access PostgreSQL databases with minimal use of sophisticated language techniques or extensions. It strikes a balance between enforcing type-safety in database interactions where it is reasonable and presenting type signatures that are minimally complicated.


Why Orville?

Orville is not meant to replace existing PostgreSQL libraries in the Haskell ecosystem, but to complement them. It has the power to satisfy most experienced Haskell developers but strives to remain approachable to newcomers despite this. Orville's API is rich enough to be used in production on large and sophisticated applications, but avoids complicated type-level programming. If your application is too large to reasonably write all your SQL statements by hand yet doesn't require absolute type-safety between your custom SQL statements, their result sets and the Haskell types they decode into, Orville may be the right choice for you.

Feature Overview

  • Rich API for marshalling Haskell types to and from SQL
  • High-level APIs for common CRUD operations
  • Optional automatic schema migrations
  • Optional API for executing complex data loads across multiple tables without ever writing an N+1 query by accident
  • Progressive escape hatches to let you dig deeper when you need to

Tutorials

See the tutorials, in order of increasing complexity:

Additional documentation is available in the Haddocks.

Just show me some code!

Ok! Here's a very simple application that inserts some entities of a Pet model and finds one of them based on its name.

module Main (main) where

import Data.Int (Int32)
import qualified Data.Text as T
import qualified Orville.PostgreSQL as O
import qualified Orville.PostgreSQL.AutoMigration as AutoMigration

{- | Pet is a plain old Haskell record that will be marshalled to and from the
  @pet@ table.
-}
data Pet =
  Pet
    { petId :: PetId
    , petName :: T.Text
    }

{- | It's good practice to create newtype specific to each entity to hold its
  primary key value
-}
newtype PetId = PetId Int32

{- | A marshaller must be defined to convert Pet to and from SQL.
-}
petMarshaller :: O.SqlMarshaller Pet Pet
petMarshaller =
  Pet
    <$> O.marshallField petId petIdField
    <*> O.marshallField petName nameField

{- | Defines the @id@ field for the marshaller to marshall the 'petId' record
  field to and from.
-}
petIdField :: O.FieldDefinition O.NotNull PetId
petIdField =
  O.coerceField (O.integerField "id")

{- | Defines the @name@ field for the marshaller to marshall the 'petName' record
  field to and from.
-}
nameField :: O.FieldDefinition O.NotNull T.Text
nameField =
  O.unboundedTextField "name"

{- | Marshaller above is associated with the @pet@ table. The marshallers fields
  will define the column of the table.
-}
petTable :: O.TableDefinition (O.HasKey PetId) Pet Pet
petTable =
  O.mkTableDefinition
    "pet"
    (O.primaryKey petIdField)
    petMarshaller

{- | A simple demo that connects to a database, inserts 2 pets and then finds the
  pet named "Spot"
-}
main :: IO ()
main = do
  pool <-
    O.createConnectionPool
      O.ConnectionOptions
        { O.connectionString = "host=localhost user=orville password=orville"
        , O.connectionNoticeReporting = O.DisableNoticeReporting
        , O.connectionPoolStripes = O.OneStripePerCapability
        , O.connectionPoolMaxConnections = O.MaxConnectionsPerStripe 1
        , O.connectionPoolLingerTime = 10
        }

  mbSpot <- O.runOrville pool insertAndFindSpot

  case mbSpot of
    Nothing -> putStrLn "No Spot Found!"
    Just _spot -> putStrLn "Spot found!"

{- | The Orville monad provides a starter pack for running Orville operations
  against a connection pool.
-}
insertAndFindSpot :: O.Orville (Maybe Pet)
insertAndFindSpot = do
  AutoMigration.autoMigrateSchema
    AutoMigration.defaultOptions
    [AutoMigration.SchemaTable petTable]

  O.insertEntity petTable $
    Pet
      { petId = PetId 1
      , petName = T.pack "FuFu"
      }

  O.insertEntity petTable $
    Pet
      { petId = PetId 2
      , petName = T.pack "Spot"
      }

  O.findFirstEntityBy
    petTable
    (O.where_ (O.fieldEquals nameField (T.pack "Spot")))