Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bluefin-opaleye/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Haskell Package Versioning Policy](https://pvp.hask

## [Unreleased]

### Changed
- Support `bluefin` versions `0.2.7 && < 0.6` in [#4](https://github.com/fpringle/bluefin-postgresql/pull/4).
- Extract common operation counting code to [postgresql-operation-counting](https://github.com/fpringle/postgresql-operation-counting) in [#6](https://github.com/fpringle/bluefin-postgresql/pull/6). Breaking change.

## [0.1.0.0] - 27.02.2026

Expand Down
4 changes: 2 additions & 2 deletions bluefin-opaleye/bluefin-opaleye.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ tested-with:
, GHC == 9.10.2

common warnings
ghc-options: -Wall -Wno-unused-do-bind -Wunused-packages
ghc-options: -Wall -Wno-unused-do-bind -Wunused-packages -Werror

common deps
build-depends:
Expand All @@ -40,7 +40,7 @@ common deps
, postgresql-simple >= 0.7 && < 0.8
, text >= 2.0 && < 2.2
, containers >= 0.6 && < 0.8
, pretty >= 1.1.1.0 && < 1.2
, postgresql-operation-counting >= 0.1 && < 0.2

common extensions
default-extensions:
Expand Down
181 changes: 16 additions & 165 deletions bluefin-opaleye/src/Bluefin/Opaleye/Count.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

{- | Thanks to our dynamic 'Opaleye' effect, we can write an alternative interpreter which,
as well as performing SQL operations as before, will also keep a tally of the number of
Expand Down Expand Up @@ -96,74 +94,28 @@ Counts at n=50: INSERT: user: 50
-}
module Bluefin.Opaleye.Count
( -- * Counting SQL operations
SQLOperationCounts (..)
, opaleyeAddCounting
opaleyeAddCounting
, withCounts

-- * Pretty-printing
, printCounts
, printCountsBrief
, renderCounts
, renderCountsBrief
, prettyCounts
, prettyCountsBrief
, module PostgreSQL.Count
)
where

import Bluefin.Compound
import Bluefin.Eff
import Bluefin.Opaleye.Effect
import Bluefin.State
import Control.Monad.IO.Class
import qualified Data.List.NonEmpty as NE
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (catMaybes, mapMaybe)
import qualified Data.Text as T
import Database.PostgreSQL.Simple.Types (QualifiedIdentifier (..))
import GHC.Generics
import Numeric.Natural
import qualified Opaleye as O
import qualified Opaleye.Internal.PrimQuery as O (TableIdentifier (..))
import qualified Opaleye.Internal.Table as O
import qualified Text.PrettyPrint as P
import qualified Text.PrettyPrint.HughesPJClass as P
import PostgreSQL.Count

------------------------------------------------------------
-- Tallying SQL operations

{- | This tracks the number of SQL operations that have been performed in the
'Opaleye' effect, along with which table it was performed on (where possible).

@INSERT@, @DELETE@ and @UPDATE@ operations act on one table only, so we can tally the number
of each that are performed on each table (indexed by a t'QualifiedIdentifier').
@SELECT@ operations can act on multiple tables, so we just track the total number of selects.

If required, t'SQLOperationCounts' can be constructed using 'Monoid' and combined using 'Semigroup'.

We use non-negative 'Natural's as a tally since a negative number of operations makes no sense.
-}
data SQLOperationCounts = SQLOperationCounts
{ sqlSelects :: Natural
, sqlInserts :: Map QualifiedIdentifier Natural
, sqlDeletes :: Map QualifiedIdentifier Natural
, sqlUpdates :: Map QualifiedIdentifier Natural
}
deriving (Show, Eq, Generic)

instance Semigroup SQLOperationCounts where
SQLOperationCounts s1 i1 d1 u1 <> SQLOperationCounts s2 i2 d2 u2 =
SQLOperationCounts
(s1 + s2)
(i1 `addNatMaps` i2)
(d1 `addNatMaps` d2)
(u1 `addNatMaps` u2)
where
addNatMaps = Map.unionWith (+)

instance Monoid SQLOperationCounts where
mempty = SQLOperationCounts 0 mempty mempty mempty

{- | Add counting of SQL operations to the interpreter of an 'Opaleye' effect.
Note that the effect itself is not actually interpreted. After updating our t'SQLOperationCounts' state
based on the 'Opaleye' constructor, we then pass them
Expand Down Expand Up @@ -208,19 +160,19 @@ opaleyeAddCounting st oldEffect k =
incrementSelect = modify st $ \counts ->
counts {sqlSelects = succ $ sqlSelects counts}

incrementInsert :: QualifiedIdentifier -> Eff (e :& es) ()
incrementInsert :: TableName -> Eff (e :& es) ()
incrementInsert name = modify st $ \counts ->
counts {sqlInserts = incrementMap name $ sqlInserts counts}

incrementUpdate :: QualifiedIdentifier -> Eff (e :& es) ()
incrementUpdate :: TableName -> Eff (e :& es) ()
incrementUpdate name = modify st $ \counts ->
counts {sqlUpdates = incrementMap name $ sqlUpdates counts}

incrementDelete :: QualifiedIdentifier -> Eff (e :& es) ()
incrementDelete :: TableName -> Eff (e :& es) ()
incrementDelete name = modify st $ \counts ->
counts {sqlDeletes = incrementMap name $ sqlDeletes counts}

incrementMap :: QualifiedIdentifier -> Map QualifiedIdentifier Natural -> Map QualifiedIdentifier Natural
incrementMap :: TableName -> Map TableName Natural -> Map TableName Natural
incrementMap = Map.alter (Just . maybe 1 succ)

-- | This allows us to count the number of SQL operations over the course of a sub-operation.
Expand All @@ -235,122 +187,21 @@ withCounts st eff = do
countsAfter <- get st
pure (countsAfter `subtractCounts` countsBefore, res)

subtractNat :: Natural -> Natural -> Natural
a `subtractNat` b = if a > b then a - b else 0

subtractNatMaps :: (Ord k) => Map k Natural -> Map k Natural -> Map k Natural
subtractNatMaps c1 c2 =
let f op count = Map.adjust (`subtractNat` count) op
in Map.foldrWithKey f c1 c2

subtractCounts :: SQLOperationCounts -> SQLOperationCounts -> SQLOperationCounts
subtractCounts (SQLOperationCounts s1 i1 d1 u1) (SQLOperationCounts s2 i2 d2 u2) =
SQLOperationCounts
(s1 `subtractNat` s2)
(i1 `subtractNatMaps` i2)
(d1 `subtractNatMaps` d2)
(u1 `subtractNatMaps` u2)

------------------------------------------------------------
-- Getting table identifiers from opaleye operations

tableIdentifierToQualifiedIdentifier :: O.TableIdentifier -> QualifiedIdentifier
tableIdentifierToQualifiedIdentifier (O.TableIdentifier mSchema table) =
QualifiedIdentifier (T.pack <$> mSchema) (T.pack table)
tableIdentifierToTableName :: O.TableIdentifier -> TableName
tableIdentifierToTableName (O.TableIdentifier mSchema table) =
TableName (T.pack <$> mSchema) (T.pack table)

insertTableName :: O.Insert haskells -> QualifiedIdentifier
insertTableName :: O.Insert haskells -> TableName
insertTableName (O.Insert table _ _ _) =
tableIdentifierToQualifiedIdentifier . O.tableIdentifier $ table
tableIdentifierToTableName . O.tableIdentifier $ table

updateTableName :: O.Update haskells -> QualifiedIdentifier
updateTableName :: O.Update haskells -> TableName
updateTableName (O.Update table _ _ _) =
tableIdentifierToQualifiedIdentifier . O.tableIdentifier $ table
tableIdentifierToTableName . O.tableIdentifier $ table

deleteTableName :: O.Delete haskells -> QualifiedIdentifier
deleteTableName :: O.Delete haskells -> TableName
deleteTableName (O.Delete table _ _) =
tableIdentifierToQualifiedIdentifier . O.tableIdentifier $ table

------------------------------------------------------------
-- Pretty rendering and printing counts

instance P.Pretty SQLOperationCounts where
pPrint = prettyCounts

{- | Print an t'SQLOperationCounts' to stdout using 'prettyCounts'.
For less verbose output, see 'printCountsBrief'.
-}
printCounts :: (MonadIO m) => SQLOperationCounts -> m ()
printCounts = liftIO . putStrLn . renderCounts

{- | Print an t'SQLOperationCounts' to stdout using 'prettyCountsBrief'.
For more verbose output, see 'printCounts'.
-}
printCountsBrief :: (MonadIO m) => SQLOperationCounts -> m ()
printCountsBrief = liftIO . putStrLn . renderCountsBrief

{- | Render an t'SQLOperationCounts' using 'prettyCounts'.
For less verbose output, see 'renderCountsBrief'.

For more control over how the 'P.Doc' gets rendered, use 'P.renderStyle' with a custom 'P.style'.
-}
renderCounts :: SQLOperationCounts -> String
renderCounts = P.render . prettyCounts

{- | Render an t'SQLOperationCounts' using 'prettyCountsBrief'.
For more verbose output, see 'renderCounts'.

For more control over how the 'P.Doc' gets rendered, use 'P.renderStyle' with a custom 'P.style'.
-}
renderCountsBrief :: SQLOperationCounts -> String
renderCountsBrief = P.render . prettyCountsBrief

{- | Pretty-print an t'SQLOperationCounts' using "Text.PrettyPrint".
For each 'Map', we'll print one line for each table. For less verbose output,
see 'prettyCountsBrief'.

This is also the implementation of 'P.pPrint' for t'SQLOperationCounts'.
-}
prettyCounts :: SQLOperationCounts -> P.Doc
prettyCounts = prettyCountsWith $ \mp ->
let counts = Map.toList mp
renderPair (name, count) = prefix (renderTableName name) <$> renderNat count
in fmap (P.vcat . NE.toList) . NE.nonEmpty $ mapMaybe renderPair counts

{- | Pretty-print an t'SQLOperationCounts' using "Text.PrettyPrint".
For each 'Map', we'll print just the sum of the counts. For more verbose output,
see 'prettyCounts'.
-}
prettyCountsBrief :: SQLOperationCounts -> P.Doc
prettyCountsBrief = prettyCountsWith $ \mp ->
let total = sum $ Map.elems mp
in renderNat total

prettyCountsWith :: (Map QualifiedIdentifier Natural -> Maybe P.Doc) -> SQLOperationCounts -> P.Doc
prettyCountsWith renderMap (SQLOperationCounts selects inserts deletes updates) =
let parts =
catMaybes
[ prefix "SELECT" <$> renderNat selects
, prefix "INSERT" <$> renderMap inserts
, prefix "UPDATE" <$> renderMap updates
, prefix "DELETE" <$> renderMap deletes
]
in case parts of
[] -> "None"
_ -> P.vcat parts

prefix :: P.Doc -> P.Doc -> P.Doc
prefix t n = t P.<> ":" P.<+> n

renderNat :: Natural -> Maybe P.Doc
renderNat = \case
0 -> Nothing
n -> Just $ P.pPrint @Integer $ toInteger n

renderTableName :: QualifiedIdentifier -> P.Doc
renderTableName (QualifiedIdentifier mSchema table) =
case mSchema of
Nothing -> renderText table
Just schema -> renderText schema <> "." <> renderText table

renderText :: T.Text -> P.Doc
renderText = P.text . T.unpack
tableIdentifierToTableName . O.tableIdentifier $ table
2 changes: 2 additions & 0 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ documentation: False

package *
documentation: False

multi-repl: True
1 change: 1 addition & 0 deletions nix/nixpkgs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ let
bluefin = hfinal.callCabal2nixWithOptions "bluefin" bluefin "--subpath bluefin" { };
postgresql-libpq = doJailbreak hprev.postgresql-libpq;
hlint = doJailbreak hprev.hlint;
postgresql-operation-counting = hfinal.callCabal2nix "postgresql-operation-counting" "${sources.postgresql-operation-counting}" { };
};

makePackage = name: path:
Expand Down
6 changes: 6 additions & 0 deletions nix/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"url": "https://github.com/NixOS/nixpkgs/archive/b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"postgresql-operation-counting": {
"branch": "main",
"repo": "git@github.com:fpringle/postgresql-operation-counting",
"rev": "140e2699399f703b331bbbf243cab1a43f182524",
"type": "git"
},
"pre-commit-hooks": {
"branch": "master",
"description": "Seamless integration of https://pre-commit.com git hooks with Nix.",
Expand Down
Loading