packages feed

postgresql-operation-counting (empty) → 0.1.0.0

raw patch · 5 files changed

+290/−0 lines, 5 filesdep +basedep +containersdep +pretty

Dependencies added: base, containers, pretty, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,20 @@+# Changelog++All notable changes to `postgresql-operation-counting` will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),+and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org).++## [Unreleased]++## [0.1.0.0] - 27.02.2026++### Added++- First edition of the package, ready for feedback.+- 100% documentation coverage.+- Reasonably detailed READMEs+- CI that builds and tests the packages for each version of GHC in the `tested-with` field.++[unreleased]: https://github.com/fpringle/postgresql-operation-counting/compare/v0.1.0.0...HEAD+[0.1.0.0]: https://github.com/fpringle/postgresql-operation-counting/releases/tag/v0.1.0.0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2026, Frederick Pringle++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Frederick Pringle nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,4 @@+# postgresql-operation-counting++For example usage, see [effectful-opaleye](https://hackage.haskell.org/package/effectful-opaleye/docs/Effectful-Opaleye-Count.html)+or [bluein-opaleye](https://hackage.haskell.org/package/bluefin-opaleye/docs/Bluefin-Opaleye-Count.html).
+ postgresql-operation-counting.cabal view
@@ -0,0 +1,59 @@+cabal-version:      3.0+name:               postgresql-operation-counting+version:            0.1.0.0+synopsis:           Track and render a tally of which PostgreSQL operations have been performed.+description:        For example usage, see [effectful-opaleye](https://hackage.haskell.org/package/effectful-opaleye/docs/Effectful-Opaleye-Count.html)+                    or [bluein-opaleye](https://hackage.haskell.org/package/bluefin-opaleye/docs/Bluefin-Opaleye-Count.html).+license:            BSD-3-Clause+license-file:       LICENSE+author:             Frederick Pringle+maintainer:         frederick.pringle@fpringle.com+copyright:          Copyright(c) Frederick Pringle 2026+homepage:           https://github.com/fpringle/postgresql-operation-counting+category:           Database+build-type:         Simple+extra-doc-files:    CHANGELOG.md+                    README.md+tested-with:+  GHC == 9.12.2+  GHC == 9.10.1+  GHC == 9.8.2+  GHC == 9.6.5+  GHC == 9.4.8+  GHC == 9.2.8+  GHC == 9.0.2+  GHC == 8.10.7+  GHC == 8.6.5++source-repository head+  type:     git+  location: https://github.com/fpringle/postgresql-operation-counting.git++common warnings+  ghc-options: -Wall -Wno-unused-do-bind -Wunused-packages++common deps+  build-depends:+    , base >= 4 && < 5+    , text >= 2.0 && < 2.2+    , containers >= 0.6 && < 0.8+    , pretty >= 1.1.1.0 && < 1.2++common extensions+  default-extensions:+    DeriveGeneric+    LambdaCase+    OverloadedStrings+    TypeApplications++library+  import:+      warnings+    , deps+    , extensions+  exposed-modules:+      PostgreSQL.Count+  other-modules:+  -- other-extensions:+  hs-source-dirs:   src+  default-language: Haskell2010
+ src/PostgreSQL/Count.hs view
@@ -0,0 +1,177 @@+module PostgreSQL.Count+  ( -- * Counting SQL operations+    SQLOperationCounts (..)+  , TableName (..)+  , subtractCounts++    -- * Pretty-printing+  , printCounts+  , printCountsBrief+  , renderCounts+  , renderCountsBrief+  , prettyCounts+  , prettyCountsBrief+  )+where++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 Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics+import Numeric.Natural+import qualified Text.PrettyPrint as P+import qualified Text.PrettyPrint.HughesPJClass as P++-- | The name of a table, optionally qualified with a schema.+data TableName = TableName+  { tableSchema :: Maybe Text+  , tableName :: Text+  }+  deriving (Show, Eq, Ord, Generic)++instance P.Pretty TableName where+  pPrint = renderTableName++------------------------------------------------------------+-- Tallying SQL operations++{- | This tracks the number of SQL operations that have been performed, along with which table they were+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'TableName').+@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', added using 'Semigroup',+and subtracted using `subtractCounts`.++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 TableName Natural+  , sqlDeletes :: Map TableName Natural+  , sqlUpdates :: Map TableName 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++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++{- | Subtract one set of counts from another. Note that the results are all still 'Natural's,+so will all be non-negative.+-}+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)++------------------------------------------------------------+-- 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 TableName 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 :: TableName -> P.Doc+renderTableName (TableName mSchema table) =+  case mSchema of+    Nothing -> renderText table+    Just schema -> renderText schema <> "." <> renderText table++renderText :: T.Text -> P.Doc+renderText = P.text . T.unpack