Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
525ea20
Try a new kind of very specialized parser for small types
mzabani Aug 8, 2026
b4d1114
Post-rebase fixing
mzabani Aug 12, 2026
05e3a9d
More specialized instances, more confirmation of benefits
mzabani Aug 12, 2026
0be5704
Specialized instance for UTCTime => clear benefits once again
mzabani Aug 12, 2026
68378ee
Tidy up a bit
mzabani Aug 12, 2026
feabc78
Slightly better understanding of inlining, clearer inlining boundaries
mzabani Aug 13, 2026
7030fd5
Add `inlinedSingleFieldDecoder`
mzabani Aug 13, 2026
3bf360b
More comprehensive coverage of types in benchmarks, more specialized …
mzabani Aug 14, 2026
53b339c
Tests for non-specialized field decoders
mzabani Aug 14, 2026
d46a807
Tidy up GHC Core
mzabani Aug 14, 2026
ef7ce8d
Overlapping Maybe instances do help with the inlined row decoder's pe…
mzabani Aug 15, 2026
2e28392
Very experimental change with `Maybe a` instances
mzabani Aug 16, 2026
fbfa92b
A separate field in FieldDecoder for what to decode NULL to
mzabani Aug 17, 2026
08cab31
Try to improve the code, but inlining got worse
mzabani Aug 18, 2026
e42a7bc
Some memory usage improvements
mzabani Aug 19, 2026
2b91148
Float decodesSqlNullTo outside and add strictness for better inlining
mzabani Aug 19, 2026
cc75921
Tidy up, a few more INLINE pragmas
mzabani Aug 19, 2026
5c7f52f
TODOs in the code
mzabani Aug 19, 2026
2a2e436
Trying a specialized notConst method
mzabani Aug 20, 2026
9c02dc1
Do the JSON types, but hpgsql-simple-compat will break
mzabani Aug 20, 2026
c0c7003
Post-rebase fixes
mzabani Aug 23, 2026
4861e73
Update TODO
mzabani Aug 23, 2026
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
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- Test both `singleField fieldDecoder` and `singleFieldRowDecoder` for every type in our tests.
- Some types (the Aeson ones, for example, but more) still don't derive specialized row decoders
- "Oh no! No colInfo here.. what do we do!?" in hpgsql-simple-compat. This might require a big rethinking of things..
- Double-check which row encoders we want to use the inlined versions for and which we don't. Tuples?
1 change: 1 addition & 0 deletions hpgsql-benchmarks/hpgsql-benchmarks.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ executable hpgsql-benchmarks
, hspec-expectations
, postgresql-simple
, resourcet
, scientific
, statistics
, stm
, streaming
Expand Down
67 changes: 47 additions & 20 deletions hpgsql-benchmarks/src/Main.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{-# OPTIONS_GHC -ddump-simpl -ddump-to-file #-}
{-# OPTIONS_GHC -ddump-simpl -dno-typeable-binds -dsuppress-coercions -dsuppress-module-prefixes -dsuppress-type-applications -ddump-to-file #-}

module Main where

Expand All @@ -24,6 +24,7 @@ import Criterion.Measurement.Types
)
import qualified Data.ByteString.Char8 as BS8
import Data.Int (Int32, Int64)
import Data.Scientific (Scientific)
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text as Text
Expand All @@ -46,6 +47,7 @@ import Hpgsql.Connection (renderLibpqConnectionString)
import qualified Hpgsql.Connection
import qualified Hpgsql.Connection as Hpgsql
import qualified Hpgsql.Copy
import Hpgsql.Encoding (inlinedSingleFieldRowDecoder)
import qualified Hpgsql.Encoding as Hpgsql
import qualified Hpgsql.Query as Hpgsql
import qualified Hpgsql.Types as Hpgsql
Expand Down Expand Up @@ -81,11 +83,19 @@ data BenchRow = BenchRow
brMaybeInt :: !(Maybe Int),
brMaybeText :: !(Maybe Text),
brMaybeDouble :: !(Maybe Double),
brMaybeDay :: !(Maybe Day)
brMaybeDay :: !(Maybe Day),
brNumeric :: !Scientific,
brFloat :: !Float,
brBool1 :: !Bool,
brBool2 :: !Bool
}
deriving stock (Generic, Show, Eq)
deriving anyclass (NFData, Hpgsql.FromPgRow, PGSimple.FromRow)

fullyInlinedBenchRowDecoder :: Hpgsql.RowDecoder BenchRow
fullyInlinedBenchRowDecoder =
BenchRow <$> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder <*> inlinedSingleFieldRowDecoder

data HasqlBenchRow = HasqlBenchRow
{ hbrId :: !Int32,
hbrDate1 :: !Day,
Expand All @@ -99,7 +109,11 @@ data HasqlBenchRow = HasqlBenchRow
hbrMaybeInt :: !(Maybe Int32),
hbrMaybeText :: !(Maybe Text),
hbrMaybeDouble :: !(Maybe Double),
hbrMaybeDay :: !(Maybe Day)
hbrMaybeDay :: !(Maybe Day),
hbrNumeric :: !Scientific,
hbrFloat :: !Float,
hbrBool1 :: Bool,
hbrBool2 :: Bool
}
deriving stock (Generic, Show, Eq)
deriving anyclass (NFData)
Expand Down Expand Up @@ -165,12 +179,14 @@ main = do

statsBefore <- getRTSStats
hspecWith defaultConfig {configFormat = Just (formatterToFormat silent)} $ do
let sql17 = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date, g::numeric, g::float4, g%2=0, g%2=1 FROM generate_series(1,$1) g"
sql17Simple = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date, g::numeric, g::float4, g%2=0, g%2=1 FROM generate_series(1,?) g"
sql13 = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,$1) g"
sql13Simple = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,?) g"
describe "Parsing 13-column rows into a List" $ do
let sql = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,$1) g"
pgSimpleSql = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,?) g"
hasqlListStmt =
let hasqlListStmt =
HasqlStmt.Statement
sql
sql13
(HasqlEnc.param (HasqlEnc.nonNullable HasqlEnc.int4))
( HasqlDec.rowList
( (,,,,,,,,,,,,)
Expand All @@ -192,7 +208,7 @@ main = do
True
hasqlRecordListStmt =
HasqlStmt.Statement
sql
sql17
(HasqlEnc.param (HasqlEnc.nonNullable HasqlEnc.int4))
( HasqlDec.rowList
( HasqlBenchRow
Expand All @@ -209,15 +225,19 @@ main = do
<*> HasqlDec.column (HasqlDec.nullable HasqlDec.text)
<*> HasqlDec.column (HasqlDec.nullable HasqlDec.float8)
<*> HasqlDec.column (HasqlDec.nullable HasqlDec.date)
<*> HasqlDec.column (HasqlDec.nonNullable HasqlDec.numeric)
<*> HasqlDec.column (HasqlDec.nonNullable HasqlDec.float4)
<*> HasqlDec.column (HasqlDec.nonNullable HasqlDec.bool)
<*> HasqlDec.column (HasqlDec.nonNullable HasqlDec.bool)
)
)
True
forM_ [10_000 :: Int, 100_000] $ \n -> do
forM_ [100_000 :: Int] $ \n -> do
it ("hpgsql Tuple List (" ++ show n ++ " rows)") $
void $
bench ("hpgsql Tuple List (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
Hpgsql.queryWith (Hpgsql.rowDecoder @(Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) conn (Hpgsql.mkQuery sql (Hpgsql.Only n))
Hpgsql.queryWith (Hpgsql.rowDecoder @(Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) conn (Hpgsql.mkQuery sql13 (Hpgsql.Only n))
it ("hasql Tuple List (" ++ show n ++ " rows)") $
void $
bench ("hasql Tuple List (" ++ show n ++ " rows)") $
Expand All @@ -228,12 +248,12 @@ main = do
void $
bench ("postgresql-simple Tuple List (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections pgSimpleConnect PGSimple.close $ \pgSimpleConn -> do
PGSimple.query @_ @(Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day) pgSimpleConn pgSimpleSql (PGSimple.Only n)
PGSimple.query @_ @(Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day) pgSimpleConn sql13Simple (PGSimple.Only n)
it ("hpgsql Record List (" ++ show n ++ " rows)") $
void $
bench ("hpgsql Record List (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
Hpgsql.queryWith (Hpgsql.rowDecoder @BenchRow) conn (Hpgsql.mkQuery sql (Hpgsql.Only n))
Hpgsql.queryWith (Hpgsql.rowDecoder @BenchRow) conn (Hpgsql.mkQuery sql17 (Hpgsql.Only n))
it ("hasql Record List (" ++ show n ++ " rows)") $
void $
bench ("hasql Record List (" ++ show n ++ " rows)") $
Expand All @@ -244,40 +264,47 @@ main = do
void $
bench ("postgresql-simple Record List (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections pgSimpleConnect PGSimple.close $ \pgSimpleConn -> do
PGSimple.query @_ @BenchRow pgSimpleConn pgSimpleSql (PGSimple.Only n)
PGSimple.query @_ @BenchRow pgSimpleConn sql17Simple (PGSimple.Only n)
describe "Parsing 13-column rows in streaming fashion" $ do
let sql = "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,$1) g"
forM_ [10_000 :: Int, 100_000] $ \n -> do
forM_ [100_000 :: Int] $ \n -> do
it ("hpgsql Tuple Stream (" ++ show n ++ " rows)") $
void $
bench ("hpgsql Tuple Stream (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
res <- Hpgsql.querySWith (Hpgsql.rowDecoder @(Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) conn (Hpgsql.mkQuery sql (Hpgsql.Only n))
res <- Hpgsql.querySWith (Hpgsql.rowDecoder @(Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) conn (Hpgsql.mkQuery sql13 (Hpgsql.Only n))
S.effects res
it ("streaming-postgresql-simple Tuple Stream (" ++ show n ++ " rows)") $
void $
bench ("streaming-postgresql-simple Tuple Stream (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections pgSimpleConnect PGSimple.close $ \pgSimpleConn -> do
runResourceT @IO $ do
let res :: Stream (Of (Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) (ResourceT IO) () = StreamingPostgresSimple.query pgSimpleConn "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,?) g" (PGSimple.Only n)
let res :: Stream (Of (Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) (ResourceT IO) () = StreamingPostgresSimple.query pgSimpleConn sql13Simple (PGSimple.Only n)
S.effects res
it ("postgresql-simple Tuple fold (" ++ show n ++ " rows)") $
void $
bench ("postgresql-simple Tuple fold (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections pgSimpleConnect PGSimple.close $ \pgSimpleConn -> do
PGSimple.fold pgSimpleConn "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,?) g" (PGSimple.Only n) () (\() (!_ :: (Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) -> pure ())
PGSimple.fold pgSimpleConn sql13Simple (PGSimple.Only n) () (\() (!_ :: (Int, Day, Day, UTCTime, UTCTime, Text, Text, Double, Double, Maybe Int, Maybe Text, Maybe Double, Maybe Day)) -> pure ())
describe "Parsing 17-column rows in streaming fashion" $ do
forM_ [100_000 :: Int] $ \n -> do
it ("hpgsql Record Stream (" ++ show n ++ " rows)") $
void $
bench ("hpgsql Record Stream (" ++ show n ++ " rows)") $ do
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
res <- Hpgsql.querySWith (Hpgsql.rowDecoder @BenchRow) conn (Hpgsql.mkQuery sql (Hpgsql.Only n))
res <- Hpgsql.querySWith (Hpgsql.rowDecoder @BenchRow) conn (Hpgsql.mkQuery sql17 (Hpgsql.Only n))
S.effects res
it ("hpgsql Record Stream (" ++ show n ++ " rows, fully inlined row decoder)") $
void $
bench ("hpgsql Record Stream (" ++ show n ++ " rows, fully inlined row decoder)") $ do
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
res <- Hpgsql.querySWith fullyInlinedBenchRowDecoder conn (Hpgsql.mkQuery sql17 (Hpgsql.Only n))
S.effects res
it ("streaming-postgresql-simple Record Stream (" ++ show n ++ " rows)") $
void $
bench ("streaming-postgresql-simple Record Stream (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections pgSimpleConnect PGSimple.close $ \pgSimpleConn -> do
runResourceT @IO $ do
let res :: Stream (Of BenchRow) (ResourceT IO) () = StreamingPostgresSimple.query pgSimpleConn "SELECT g, ('2000-01-01'::date + g::int4), ('2000-06-15'::date + g::int4), ('2000-01-01T00:00:00Z'::timestamptz + g * interval '1 second'), ('2020-06-15T12:00:00Z'::timestamptz + g * interval '1 minute'), 'row-' || g::text, 'item-' || g::text, g::float8 * 1.5, g::float8 * 2.5, NULL::int4, NULL::text, NULL::float8, NULL::date FROM generate_series(1,?) g" (PGSimple.Only n)
let res :: Stream (Of BenchRow) (ResourceT IO) () = StreamingPostgresSimple.query pgSimpleConn sql17Simple (PGSimple.Only n)
S.effects res
it ("postgresql-simple Record fold (" ++ show n ++ " rows)") $
void $
Expand Down
10 changes: 7 additions & 3 deletions hpgsql-simple-compat/src/Database/PostgreSQL/Simple/FromField.hs
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,13 @@ class FromField a where
let dec = Hpgsql.fieldDecoder
in \f ->
if Hpgsql.allowedPgTypes dec f
then \mbs -> Conversion $ \_encCtx -> case Hpgsql.fieldValueDecoder dec f mbs of
Right v -> Ok v
Left err -> Errors [toException $ userError err]
then \mbs -> Conversion $ \_encCtx -> case mbs of
Nothing -> case dec.decodesSqlNullTo of
Left err -> Errors [toException $ userError err]
Right v -> Ok v
Just bs -> case Hpgsql.fieldValueDecoder dec f bs of
Right v -> Ok v
Left err -> Errors [toException $ userError err]
else \_ -> Conversion $ \_encCtx -> Errors [toException $ userError "Invalid type OID for FromField instance"]

instance FromField ()
Expand Down
21 changes: 16 additions & 5 deletions hpgsql-simple-compat/src/Database/PostgreSQL/Simple/HpgsqlUtils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,29 @@ type FieldParser a = Field -> Maybe ByteString -> Conversion a
toHpgsqlFieldDecoder :: FieldParser a -> FieldDecoder a
toHpgsqlFieldDecoder fp =
FieldDecoder
{ fieldValueDecoder = \colInfo mbs ->
let valConv = fp colInfo mbs
{ fieldValueDecoder = \colInfo bs ->
let valConv = fp colInfo (Just bs)
in case runConversion valConv colInfo.encodingContext of
Ok v -> Right v
Errors errs -> Left (show errs),
decodesSqlNullTo =
let valConv = fp (error "Oh no! No colInfo here.. what do we do!?") Nothing
encCtx = error "We could fake an EncodingContext, at least. TODO."
in case runConversion valConv encCtx of
Ok v -> Right v
Errors errs -> Left (show errs),
allowedPgTypes = const True -- No way to check if types are valid ahead of time
}

fromHpgsqlFieldDecoder :: FieldDecoder a -> FieldParser a
fromHpgsqlFieldDecoder dec = \f mbs -> Conversion $ \_encCtx -> case dec.fieldValueDecoder f mbs of
Right v -> Ok v
Left err -> Errors [toException $ userError $ show err]
fromHpgsqlFieldDecoder dec = \f mbs -> Conversion $ \_encCtx ->
case mbs of
Nothing -> case dec.decodesSqlNullTo of
Left err -> Errors [toException $ userError $ show err]
Right v -> Ok v
Just bs -> case dec.fieldValueDecoder f bs of
Right v -> Ok v
Left err -> Errors [toException $ userError $ show err]

-- | Given a Hpgsql query, returns the text format with question marks
-- for query arguments and a row object. With both, you can call
Expand Down
Loading