Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
941926f
Claude-written rust benchmarks using tokio-postgres
mzabani Aug 8, 2026
7ff38f0
Improve benchmarks and peak memory measurement methodology
mzabani Sep 5, 2026
fb63872
Tidy up
mzabani Sep 5, 2026
d5b6943
A special Nix shell for benchmarks
mzabani Sep 5, 2026
7066356
Add a C# benchmark
mzabani Sep 6, 2026
2fd85dc
Tidy up a bit more
mzabani Sep 6, 2026
c1668bf
Rewrite benchmark script in Nushell
mzabani Sep 6, 2026
bc0a088
Add Rust and C# equivalents of the "Record List" benchmark
mzabani Sep 6, 2026
613c248
Try a new kind of very specialized parser for small types
mzabani Aug 8, 2026
8a79a66
Float decodesSqlNullTo outside and add strictness for better inlining
mzabani Aug 19, 2026
3f3a630
Trying a specialized notConst method
mzabani Aug 20, 2026
7d0d744
Create new strict and lazy ByteString-like PinnedByteArray types
mzabani Aug 14, 2026
a2cd77d
Wire up new benchmarks
mzabani Sep 6, 2026
a648f7e
Change `FieldDecoder` back to what it was in `master`
mzabani Sep 8, 2026
a4d6779
Rewrite rules to transform `singleField fieldDecoder` to `notInlinedS…
mzabani Sep 12, 2026
7a0c7e8
Improve benchmarks for `Maybe a` instances
mzabani Sep 12, 2026
e8e4fee
Add rewrite rules for many (Maybe a) decoders
mzabani Sep 12, 2026
fc8a359
Make queries in C# and Rust match Haskell's
mzabani Sep 12, 2026
ba524a1
Update TODO
mzabani Sep 12, 2026
31fa9e0
Tidy up and avoid breaking changes
mzabani Sep 12, 2026
5f1c5ea
A few more types with specialized row decoders and rewrite rules
mzabani Sep 12, 2026
80a0fb1
Actually test Field Decoders in tests now
mzabani Sep 12, 2026
6c9186b
Important TODO entry
mzabani Sep 12, 2026
590eb85
Expose the not inlined method and choose better names
mzabani Sep 12, 2026
d579188
Important TODO entry
mzabani Sep 12, 2026
e0e7ec5
More specialized instances
mzabani Sep 13, 2026
b5cdf67
Faster array decoders
mzabani Sep 13, 2026
6284506
Faster UUID decoder
mzabani Sep 13, 2026
b84e153
Make `Int` decoding a tiny bit faster with the not-const field decoder
mzabani Sep 17, 2026
d2778d5
Speed up enums and Lazy Text, replace one `error` with a proper failure
mzabani Sep 17, 2026
81ba988
Apply new decoding pattern to `String`
mzabani Sep 17, 2026
954dc8c
Apply same pattern to many more tests
mzabani Sep 17, 2026
82d7d46
Move each pair of rewrite rules closer to their inlined bindings
mzabani Sep 17, 2026
edb7f89
The `Int` parser can be simplified a bit
mzabani Sep 17, 2026
d755821
Specialized row decoders for arrays
mzabani Sep 17, 2026
bf69deb
Specialized row decoders for arrays
mzabani Sep 17, 2026
bd9b6ec
Specialized row decoders for json types
mzabani Sep 19, 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
2 changes: 1 addition & 1 deletion Runfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ tests:
if [ -n "$NIX" ]; then
nix-build --no-out-link -A "testsPg${pg}" --argstr hspecArgs "$TARGS"
else
cabal build hpgsql-tests hpgsql-simple-compat-tests
cabal build hpgsql-tests # hpgsql-simple-compat-tests
nix-shell -A "shellPg${pg}" ./default.nix --run "./scripts/run-tests-db-internal.sh $TARGS"
fi
done
Expand Down
8 changes: 8 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- Test both `singleField fieldDecoder` and `singleFieldRowDecoder` for every type in our tests.
- For array types too we have more than one arrayField* variant
- Some types might still not derive specialized row decoders
- And rewrite rules too are missing for many
- Text internals usage.. is it safe? Double-check.
- Check that we're not holding on to internal buffers when Record fields being materialized into aren't strict
- Write property-based tests for PinnedByteArray functions
- Double check that for this PR the inlined and not inlined versions really differ in performance
6 changes: 3 additions & 3 deletions csharp-benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ string FormatSecs(double s)
'item-' || g::text,
g::float8 * 1.5,
g::float8 * 2.5,
NULL::int4,
NULL::text,
NULL::float8,
(CASE WHEN g%2=1 THEN NULL ELSE g::int4 END),
(CASE WHEN g%2=1 THEN NULL ELSE '' END)::text,
(CASE WHEN g%2=1 THEN NULL ELSE 0.0 END)::float8,
NULL::date,
g::numeric,
g::float4,
Expand Down
46 changes: 39 additions & 7 deletions hpgsql-benchmarks/src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import Hpgsql.Connection (renderLibpqConnectionString)
import qualified Hpgsql.Connection
import qualified Hpgsql.Connection as Hpgsql
import qualified Hpgsql.Copy
import Hpgsql.Encoding (inlinedFieldRowDecoder)
import qualified Hpgsql.Encoding as Hpgsql
import qualified Hpgsql.Query as Hpgsql
import qualified Hpgsql.Types as Hpgsql
Expand Down Expand Up @@ -91,6 +92,14 @@ data BenchRow = BenchRow
deriving stock (Generic, Show, Eq)
deriving anyclass (NFData, Hpgsql.FromPgRow, PGSimple.FromRow)

singleFieldBenchRowDecoder :: Hpgsql.RowDecoder BenchRow
singleFieldBenchRowDecoder =
BenchRow <$> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder <*> Hpgsql.singleField Hpgsql.fieldDecoder

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

data HasqlBenchRow = HasqlBenchRow
{ hbrId :: !Int32,
hbrDate1 :: !Day,
Expand Down Expand Up @@ -183,10 +192,10 @@ main = do
statsBefore <- getRTSStats
hspecWith defaultConfig {configFormat = Just (formatterToFormat silent)} $ do
let n :: Int = 100_000
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"
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, (CASE WHEN g%2=1 THEN NULL ELSE g::int4 END), (CASE WHEN g%2=1 THEN NULL ELSE '' END)::text, (CASE WHEN g%2=1 THEN NULL ELSE 0.0 END)::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, (CASE WHEN g%2=1 THEN NULL ELSE g::int4 END), (CASE WHEN g%2=1 THEN NULL ELSE '' END)::text, (CASE WHEN g%2=1 THEN NULL ELSE 0.0 END)::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, (CASE WHEN g%2=1 THEN NULL ELSE g END)::int4, (CASE WHEN g%2=1 THEN NULL ELSE '' END)::text, (CASE WHEN g%2=1 THEN NULL ELSE 0.0 END)::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, (CASE WHEN g%2=1 THEN NULL ELSE g END)::int4, (CASE WHEN g%2=1 THEN NULL ELSE '' END)::text, (CASE WHEN g%2=1 THEN NULL ELSE 0.0 END)::float8, NULL::date FROM generate_series(1,?) g"
describe "Parsing 13-column rows into a List" $ do
let hasqlListStmt =
HasqlStmt.Statement
Expand Down Expand Up @@ -257,6 +266,16 @@ main = do
bench ("hpgsql Record List (" ++ show n ++ " rows, Generically derived row decoder)") $
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
Hpgsql.queryWith (Hpgsql.rowDecoder @BenchRow) conn (Hpgsql.mkQuery sql17 (Hpgsql.Only n))
it ("hpgsql Record List (" ++ show n ++ " rows, fully inlined row decoder)") $
void $
bench ("hpgsql Record List (" ++ show n ++ " rows, fully inlined row decoder)") $
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
Hpgsql.queryWith fullyInlinedBenchRowDecoder conn (Hpgsql.mkQuery sql17 (Hpgsql.Only n))
it ("hpgsql Record List (" ++ show n ++ " rows, `singleField fieldDecoder` row decoder)") $
void $
bench ("hpgsql Record List (" ++ show n ++ " rows, `singleField fieldDecoder` row decoder)") $
withMultipleConnections numConcurrentConnections hpgsqlConnect Hpgsql.Connection.closeGracefully $ \conn -> do
Hpgsql.queryWith singleFieldBenchRowDecoder conn (Hpgsql.mkQuery sql17 (Hpgsql.Only n))
it ("hasql Record List (" ++ show n ++ " rows)") $
void $
bench ("hasql Record List (" ++ show n ++ " rows)") $
Expand Down Expand Up @@ -301,11 +320,24 @@ main = do
runResourceT @IO $ do
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, Generically derived row decoder)") $
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 sql17Simple (PGSimple.Only n)
S.effects res
it ("postgresql-simple Record fold (" ++ show n ++ " rows)") $
void $
bench ("postgresql-simple Record fold (" ++ show n ++ " rows, Generically derived row decoder)") $
bench ("postgresql-simple Record fold (" ++ show n ++ " rows)") $
withMultipleConnections numConcurrentConnections pgSimpleConnect PGSimple.close $ \pgSimpleConn -> do
PGSimple.fold pgSimpleConn sql17Simple (PGSimple.Only n) () (\() (!_ :: BenchRow) -> pure ())
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) () (\() (!_ :: BenchRow) -> pure ())
describe "COPY FROM STDIN" $ do
(conn, pgSimpleConn) <- runIO $ do
hpgsqlConnInfo <- testConnInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ toHpgsqlFieldDecoder fp =
}

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 dec.fieldValueDecoder f mbs 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
Loading