From 423f28f0a2ddf0b7ea45bfa254eebce981a468b5 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Sat, 16 Sep 2023 19:52:35 +0200 Subject: [PATCH] Add quick start section to readme --- README.md | 54 ++++++++++++++++++++++++++- test/Database/PostgreSQL/OpiumSpec.hs | 12 ++++++ test/SpecHook.hs | 4 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ec0c7a2..2eb0c1b 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,66 @@ > An opionated Haskell Postgres library. +## Quick Start + +We assume that our database contains this table: + +```sql +CREATE TABLE person ( + name TEXT NOT NULL, + age INT NOT NULL, + score DOUBLE PRECISION NOT NULL, + motto TEXT +) +``` + +We can use `opium` to decode query results into Haskell data types: + +```haskell +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} + +import GHC.Generics (Generic) +import Database.PostgreSQL.LibPQ (Connection) +import qualified Database.PostgreSQL.Opium as Opium + +data User = User + { name :: String + , age :: Int + , score :: Double + } deriving (Eq, Generic, Show) + +instance Opium.FromRow User where + +getUsers :: Connection -> IO (Either Opium.Error [Users]) +getUsers conn = Opium.fetch_ conn "SELECT * FROM user" +``` + +The `Opium.FromRow` instance is implemented generically for all product types ("records"). It looks up the field name in the query result and decodes the column value using `Opium.FromField`. + +The intended use case for this library is to enable us to write ad-hoc types for query results without having to manually write instances. +For example, if we wanted to figure out how user age influences their score: + +```haskell +data ScoreByAge = ScoreByAge { t :: Double, m :: Double } + deriving (Eq, Generic, Show) + +getScoreByAge :: Connection -> IO ScoreByAge +getScoreByAge conn = do + let query = "SELECT regr_intercept(score, age) AS t, regr_slope(score, age) AS m FROM user" + Right [x] <- Opium.fetch_ conn query + pure x +``` + ## TO DO - [x] Implement `String` and `Text` decoding - [x] Implement `Int` decoding - [x] Implement error reporting i.e. use `Either OpiumError` instead of `Maybe` - [x] Implement `Float` and `Double` decoding +- [x] Clean up and document column table stuff +- [ ] Implement `fetch` (`fetch_` but with parameter passing) - [ ] Implement `UTCTime` and zoned time decoding - [ ] Implement JSON decoding - [ ] Implement `ByteString` decoding (`bytea`) - Can we make the fromField instance choose whether it wants binary or text? -- [x] Clean up and document column table stuff diff --git a/test/Database/PostgreSQL/OpiumSpec.hs b/test/Database/PostgreSQL/OpiumSpec.hs index 3fb3d6b..1c82e23 100644 --- a/test/Database/PostgreSQL/OpiumSpec.hs +++ b/test/Database/PostgreSQL/OpiumSpec.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} @@ -38,6 +39,13 @@ data ManyFields = ManyFields instance Opium.FromRow ManyFields where +data ScoreByAge = ScoreByAge + { m :: Double + , t :: Double + } deriving (Eq, Generic, Show) + +instance Opium.FromRow ScoreByAge where + isLeft :: Either a b -> Bool isLeft (Left _) = True isLeft _ = False @@ -125,3 +133,7 @@ spec = do it "Fails for the wrong column type" $ \conn -> do rows <- Opium.fetch_ @Person conn "SELECT 'quby' AS name, 'indeterminate' AS age" rows `shouldBe` Left (Opium.ErrorInvalidOid "age" $ LibPQ.Oid 25) + + it "Works for the readme regression example" $ \conn -> do + rows <- Opium.fetch_ @ScoreByAge conn "SELECT regr_intercept(score, age) AS t, regr_slope(score, age) AS m FROM person" + rows `shouldSatisfy` \case { (Right [ScoreByAge _ _]) -> True; _ -> False } diff --git a/test/SpecHook.hs b/test/SpecHook.hs index 07fa76c..9fc7d3e 100644 --- a/test/SpecHook.hs +++ b/test/SpecHook.hs @@ -23,8 +23,8 @@ setupConnection = do conn <- LibPQ.connectdb $ Encoding.encodeUtf8 $ Text.pack dsn _ <- LibPQ.setClientEncoding conn "UTF8" - _ <- LibPQ.exec conn "CREATE TABLE person (name TEXT NOT NULL, age INT NOT NULL, motto TEXT)" - _ <- LibPQ.exec conn "INSERT INTO person VALUES ('paul', 25), ('albus', 103)" + _ <- LibPQ.exec conn "CREATE TABLE person (name TEXT NOT NULL, age INT NOT NULL, score DOUBLE PRECISION NOT NULL, motto TEXT)" + _ <- LibPQ.exec conn "INSERT INTO person VALUES ('paul', 25, 30), ('albus', 103, 50.42)" pure conn