Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e3388bc
feat: make PgBinaryValue readonly, update implementations, tests and …
ClasicRando Mar 11, 2026
994016a
chore: clean up some API elements and docs, format code
ClasicRando Mar 16, 2026
a4424c1
feat: create base IPgDataRow class to add ability to result set and r…
ClasicRando Mar 17, 2026
c5cb1b1
feat: remove PipeWriter for IBufferWriter, create IBufferReader to ma…
ClasicRando Mar 18, 2026
1cdc065
feat: add IPgBinaryCopyRow source generation, use source generation i…
ClasicRando Mar 19, 2026
7f7b31c
fix: perform correct check for initial capacity
ClasicRando Mar 19, 2026
69b3c63
feat: add examples project
ClasicRando Mar 19, 2026
c974244
feat: add tests for ToParam and ToPgBinaryCopyRow source generation a…
ClasicRando Mar 19, 2026
0628481
feat: rework source generation to reduce duplication, add accessibili…
ClasicRando Mar 26, 2026
42600cf
fix: implement methods that were erroneously recursive
ClasicRando Mar 26, 2026
a9a8a71
feat: add wrapper type source generation implemention
ClasicRando Mar 26, 2026
428778a
fix: issue with array type full name appending
ClasicRando Mar 26, 2026
ba98e58
feat: rewrite FromRow source generation to use standard IPgDataRow me…
ClasicRando Mar 26, 2026
15b51d6
fix: issue with generic type name appending, add index check for IPgD…
ClasicRando Mar 26, 2026
e0028cb
chore: remove unused methods
ClasicRando Mar 26, 2026
b2c4865
feat: simplify projects by removing named decode methods, remove inte…
ClasicRando Mar 27, 2026
9f1ec1d
chore: update docs to match updated driver/library functionality
ClasicRando Mar 27, 2026
96257ee
feat: implement source interceptor to simplify IPgDataRow field extra…
ClasicRando Mar 27, 2026
9527944
feat: remove base type extraction methods, use GetField where possibl…
ClasicRando Mar 28, 2026
2dc750f
feat: remove unnecessary bind methods, prep for removing of primitive…
ClasicRando Mar 28, 2026
959bee7
feat: create bind interceptor, remove wrapping enum generator, includ…
ClasicRando Mar 28, 2026
9e9cb63
fix: issues with source generation and interception missing certain t…
ClasicRando Apr 19, 2026
8b8bc8d
feat: add source interceptor for ExecuteScalar, fix small issues with…
ClasicRando Apr 19, 2026
903d690
chore: revert some unintended changes, include source interceptor int…
ClasicRando Apr 19, 2026
95452c3
feat: ensure any project that depends upon sqlx-cs-pg will also get g…
ClasicRando Apr 20, 2026
e06fbb3
Merge pull request #2 from ClasicRando/source-interceptor
ClasicRando Apr 20, 2026
bed5f6b
fix: test regression
ClasicRando Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 1 addition & 8 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ The interface can be implemented manually or source generated. The source genera
implemented for each database and might provide different options. Example of a database
implementation:
```c#
// Each database driver will have it's own source generator due to custom types
[Sqlx.Postgres.Generated.FromRow]
partial struct Row
{
Expand Down Expand Up @@ -176,11 +177,3 @@ than it could be a future addition.
#### Why does everything involve extension methods?
As mentioned [here](#implement-general-use-cases-as-extension-methods), extension methods are the
best way to add behaviour and compose multiple concepts/types into a single general use case.

There are also cases where extension methods may seem a little overkill such as row field
deserialization. Honestly, I agree that it's a little convoluted, but it's the only way to add the
ability to deserialize certain types without making a general `IDataRow.Get<T>`. These types of
generic methods without bounds are rife for developers to add values that can never be deserialized,
and you won't know until you run your application. With that in mind, we hope to remove those
methods when this [feature](https://github.com/dotnet/csharplang/issues/9319) is finally implemented
in C#.
70 changes: 48 additions & 22 deletions benchmarks/Benchmarks.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Benchmarks
## DISCLAIMER
Micro-benchmarks are never great representations of true comparison between libraries/tools. Results
may vary wildly between libraries under different conditions so take all the output below as
cherry-picked outcomes that make `sqlx-cs` look much better than it actually is. To get a good idea
as to how the libraries compare, take some of the main queries your application runs and see how
they perform under similar conditions your applications is under. Then run your system under load
with both libraries to get the true comparison.

## Queries
### Overview
Benchmarks are run using BenchmarkDotNet and run the same SQL query and deserialization. Init SQL:
```postgresql
```sql
DROP TABLE IF EXISTS public.posts;
CREATE TABLE public.posts (
id int primary key generated always as identity,
Expand All @@ -17,7 +25,7 @@ SELECT REPEAT('x', 2000), current_timestamp, current_timestamp
FROM generate_series(1, 5000) s
```
Queries executed during benchmarks:
```postgresql
```sql
-- Single row
SELECT id, text_field, creation_date, last_change_date, counter
FROM public.posts
Expand All @@ -39,24 +47,25 @@ is minimal difference between the 2 drivers.

| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio |
|------------|------------------------------------------------|-------------|-------------|-------------|-------------|-------|---------|------------|------------|-----------|--------------|-------------|
| Npgsql | Batched Queries | 337.9 us | 13.53 us | 38.83 us | 330.0 us | 1.01 | 0.16 | - | - | - | 53.48 KB | 1.00 |
| sqlx-cs-pg | Batched Queries | 254.8 us | 9.54 us | 27.05 us | 251.8 us | 0.76 | 0.12 | - | - | - | 51.4 KB | 0.96 |
| Npgsql | Batched Queries | 370.6 us | 19.20 us | 55.10 us | 356.1 us | 1.02 | 0.21 | - | - | - | 53.48 KB | 1.00 |
| sqlx-cs-pg | Batched Queries | 239.7 us | 11.55 us | 32.94 us | 235.8 us | 0.66 | 0.13 | - | - | - | 50.87 KB | 0.95 |
| | | | | | | | | | | | | |
| Npgsql | Simple Query, All Rows | 13,671.0 us | 780.23 us | 2,263.58 us | 13,259.5 us | 1.03 | 0.24 | 1000.0000 | - | - | 20309.25 KB | 1.00 |
| sqlx-cs-pg | Simple Query, All Rows | 12,771.2 us | 1,148.92 us | 3,183.64 us | 11,856.5 us | 0.96 | 0.29 | 1000.0000 | - | - | 20528.64 KB | 1.01 |
| Npgsql | Simple Query, All Rows | 12,880.9 us | 947.10 us | 2,747.71 us | 12,087.2 us | 1.04 | 0.30 | 1000.0000 | - | - | 20324.52 KB | 1.00 |
| sqlx-cs-pg | Simple Query, All Rows | 11,197.4 us | 1,374.07 us | 4,008.23 us | 12,995.0 us | 1.09 | 0.39 | 1000.0000 | - | - | 20342.31 KB | 1.00 |
| | | | | | | | | | | | | |
| Npgsql | Simple Query, Concurrent Connections, All Rows | 70,377.9 us | 1,406.02 us | 3,679.30 us | 70,570.3 us | 1.00 | 0.07 | 14000.0000 | 13000.0000 | 2000.0000 | 202920.96 KB | 1.00 |
| sqlx-cs-pg | Simple Query, Concurrent Connections, All Rows | 69,889.5 us | 1,389.41 us | 3,660.27 us | 69,462.0 us | 1.00 | 0.07 | 13000.0000 | 12000.0000 | 1000.0000 | 205268.41 KB | 1.01 |
| Npgsql | Simple Query, Concurrent Connections, All Rows | 69,390.3 us | 1,651.31 us | 4,764.39 us | 68,777.7 us | 1.00 | 0.10 | 14000.0000 | 13000.0000 | 2000.0000 | 202915.69 KB | 1.00 |
| sqlx-cs-pg | Simple Query, Concurrent Connections, All Rows | 64,849.7 us | 1,664.55 us | 4,749.05 us | 64,628.6 us | 0.94 | 0.09 | 13000.0000 | 12000.0000 | 1000.0000 | 202916.34 KB | 1.00 |
| | | | | | | | | | | | | |
| Npgsql | Simple Query, Multi Row | 327.4 us | 10.10 us | 29.30 us | 326.2 us | 1.01 | 0.13 | - | - | - | 48.48 KB | 1.00 |
| sqlx-cs-pg | Simple Query, Multi Row | 229.2 us | 7.59 us | 21.65 us | 231.0 us | 0.71 | 0.09 | - | - | - | 47.18 KB | 0.97 |
| Npgsql | Simple Query, Multi Row | 308.5 us | 10.67 us | 30.25 us | 309.5 us | 1.01 | 0.14 | - | - | - | 48.48 KB | 1.00 |
| sqlx-cs-pg | Simple Query, Multi Row | 214.1 us | 6.46 us | 18.23 us | 216.6 us | 0.70 | 0.09 | - | - | - | 46.73 KB | 0.96 |
| | | | | | | | | | | | | |
| Npgsql | Simple Query, Single Row | 239.2 us | 6.59 us | 18.37 us | 235.6 us | 1.01 | 0.11 | - | - | - | 7.38 KB | 1.00 |
| sqlx-cs-pg | Simple Query, Single Row | 155.6 us | 3.93 us | 11.35 us | 154.2 us | 0.65 | 0.07 | - | - | - | 6.43 KB | 0.87 |
| Npgsql | Simple Query, Single Row | 236.2 us | 4.69 us | 13.00 us | 234.5 us | 1.00 | 0.08 | - | - | - | 7.38 KB | 1.00 |
| sqlx-cs-pg | Simple Query, Single Row | 154.9 us | 4.47 us | 12.91 us | 153.4 us | 0.66 | 0.06 | - | - | - | 6.45 KB | 0.87 |

## PostgreSQL COPY
### Overview
Benchmarks are run using BenchmarkDotNet and run the same SQL query and copy data. Init SQL:
```postgresql
```sql
DROP TABLE IF EXISTS public.copy_target;
CREATE TABLE public.copy_target(
id int primary key,
Expand All @@ -65,25 +74,42 @@ CREATE TABLE public.copy_target(
last_change_date timestamp not null,
counter int
);

DROP TABLE IF EXISTS public.copy_source;
CREATE TABLE public.copy_source(
id int primary key,
text_field text not null,
creation_date timestamp not null,
last_change_date timestamp not null,
counter int
);

INSERT INTO public.copy_source(id, text_field, creation_date, last_change_date)
SELECT s.a, REPEAT('x', 2000), current_timestamp, current_timestamp
FROM generate_series(1, 5000) AS s(a);
```
Queries executed during benchmarks:
```postgresql
```sql
COPY public.copy_target FROM STDIN WITH (FORMAT CSV);

COPY public.copy_target FROM STDIN WITH (FORMAT binary);

COPY public.copy_target TO STDOUT WITH (FORMAT CSV);

COPY public.copy_target TO STDOUT WITH (FORMAT binary);
```

### Results
| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|------------|-----------------|----------|----------|----------|----------|-------|---------|-----------|-------------|-------------|
| Npgsql | CopyIn, Binary | 66.76 ms | 1.500 ms | 4.350 ms | 65.93 ms | 1.00 | 0.09 | - | 1566.61 KB | 1.00 |
| sqlx-cs-pg | CopyIn, Binary | 69.67 ms | 1.381 ms | 3.564 ms | 68.08 ms | 1.05 | 0.08 | - | 1572.19 KB | 1.00 |
| Npgsql | CopyIn, Binary | 64.76 ms | 1.292 ms | 3.471 ms | 63.90 ms | 1.00 | 0.07 | - | 1566.91 KB | 1.00 |
| sqlx-cs-pg | CopyIn, Binary | 68.79 ms | 1.373 ms | 3.850 ms | 67.25 ms | 1.07 | 0.08 | - | 1556.12 KB | 0.99 |
| | | | | | | | | | | |
| Npgsql | CopyIn, CSV | 86.91 ms | 1.734 ms | 4.350 ms | 87.04 ms | 1.00 | 0.07 | - | 4.76 KB | 1.00 |
| sqlx-cs-pg | CopyIn, CSV | 87.62 ms | 1.748 ms | 4.544 ms | 87.39 ms | 1.01 | 0.07 | - | 3.81 KB | 0.80 |
| Npgsql | CopyIn, CSV | 87.39 ms | 1.742 ms | 4.241 ms | 87.08 ms | 1.00 | 0.07 | - | 4.76 KB | 1.00 |
| sqlx-cs-pg | CopyIn, CSV | 83.94 ms | 1.671 ms | 3.488 ms | 82.92 ms | 0.96 | 0.06 | - | 3.8 KB | 0.80 |
| | | | | | | | | | | |
| Npgsql | CopyOut, Binary | 15.44 ms | 2.086 ms | 6.118 ms | 12.44 ms | 1.14 | 0.60 | 1000.0000 | 20300.69 KB | 1.00 |
| sqlx-cs-pg | CopyOut, Binary | 14.61 ms | 1.432 ms | 4.154 ms | 13.24 ms | 1.08 | 0.47 | 1000.0000 | 20530.5 KB | 1.01 |
| Npgsql | CopyOut, Binary | 17.00 ms | 1.659 ms | 4.814 ms | 15.38 ms | 1.08 | 0.42 | 1000.0000 | 20290.66 KB | 1.00 |
| sqlx-cs-pg | CopyOut, Binary | 14.72 ms | 1.472 ms | 4.271 ms | 13.80 ms | 0.93 | 0.37 | 1000.0000 | 20298.46 KB | 1.00 |
| | | | | | | | | | | |
| Npgsql | CopyOut, CSV | 17.81 ms | 0.679 ms | 1.948 ms | 17.22 ms | 1.01 | 0.15 | - | 526.79 KB | 1.00 |
| sqlx-cs-pg | CopyOut, CSV | 18.65 ms | 0.827 ms | 2.425 ms | 17.77 ms | 1.06 | 0.17 | - | 117.51 KB | 0.22 |
| Npgsql | CopyOut, CSV | 17.69 ms | 0.544 ms | 1.551 ms | 17.37 ms | 1.01 | 0.12 | - | 586.63 KB | 1.00 |
| sqlx-cs-pg | CopyOut, CSV | 18.11 ms | 0.831 ms | 2.410 ms | 17.19 ms | 1.03 | 0.16 | - | 199.76 KB | 0.34 |
12 changes: 3 additions & 9 deletions benchmarks/IdPairParam.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
using Sqlx.Core.Query;
using Sqlx.Postgres.Query;
using Sqlx.Postgres.Generator.Query;

namespace benchmarks;

public readonly struct IdPairParam : IBindMany<IPgBindable>
[ToParam]
public readonly partial struct IdPairParam
{
public required int Id1 { get; init; }

public required int Id2 { get; init; }

public void BindMany(IPgBindable bindable)
{
bindable.Bind(Id1);
bindable.Bind(Id2);
}
}
11 changes: 3 additions & 8 deletions benchmarks/IdParam.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
using Sqlx.Core.Query;
using Sqlx.Postgres.Query;
using Sqlx.Postgres.Generator.Query;

namespace benchmarks;

public readonly struct IdParam : IBindMany<IPgBindable>
[ToParam]
public readonly partial struct IdParam
{
public required int Id { get; init; }

public void BindMany(IPgBindable bindable)
{
bindable.Bind(Id);
}
}
1 change: 0 additions & 1 deletion benchmarks/PostgresBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using NpgsqlTypes;
using Sqlx.Core.Pool;
using Sqlx.Core.Result;
using Sqlx.Postgres;
using Sqlx.Postgres.Connection;
using Sqlx.Postgres.Pool;
using Sqlx.Postgres.Query;
Expand Down
9 changes: 4 additions & 5 deletions benchmarks/PostgresCopyBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Npgsql;
using Sqlx.Core.Pool;
using Sqlx.Core.Result;
using Sqlx.Postgres;
using Sqlx.Postgres.Connection;
using Sqlx.Postgres.Copy;
using Sqlx.Postgres.Pool;
Expand Down Expand Up @@ -186,7 +185,7 @@ public async Task<List<RowData>> CopyOutBinaryNpgsql()
public async Task<QueryResult> CopyInCsvSqlx()
{
await using IPgConnection connection = _sqlxPgConnectionPool.CreateConnection();
TableFromCsv copyStatement = new()
CopyTableFromCsv copyStatement = new()
{
SchemaName = "public",
TableName = "copy_target",
Expand All @@ -198,7 +197,7 @@ public async Task<QueryResult> CopyInCsvSqlx()
public async Task<QueryResult> CopyInBinarySqlx()
{
await using IPgConnection connection = _sqlxPgConnectionPool.CreateConnection();
TableFromBinary copyStatement = new()
CopyTableFromBinary copyStatement = new()
{
SchemaName = "public",
TableName = "copy_target",
Expand All @@ -210,7 +209,7 @@ public async Task<QueryResult> CopyInBinarySqlx()
public async Task CopyOutCsvSqlx()
{
await using IPgConnection connection = _sqlxPgConnectionPool.CreateConnection();
TableToCsv copyStatement = new()
CopyTableToCsv copyStatement = new()
{
SchemaName = "public",
TableName = "copy_source",
Expand All @@ -222,7 +221,7 @@ public async Task CopyOutCsvSqlx()
public async Task<List<RowData>> CopyOutBinarySqlx()
{
await using IPgConnection connection = _sqlxPgConnectionPool.CreateConnection();
TableToBinary copyStatement = new()
CopyTableToBinary copyStatement = new()
{
SchemaName = "public",
TableName = "copy_source",
Expand Down
19 changes: 3 additions & 16 deletions benchmarks/RowData.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
using CsvHelper.Configuration.Attributes;
using Sqlx.Core.Query;
using Sqlx.Postgres.Copy;
using Sqlx.Postgres.Generator;
using Sqlx.Postgres.Generator.Copy;
using Sqlx.Postgres.Generator.Result;
using Sqlx.Postgres.Query;

namespace benchmarks;

[FromRow(RenameAll = Rename.SnakeCase)]
public readonly partial struct RowData : IPgBinaryCopyRow
[FromRow(RenameAll = Rename.SnakeCase), ToPgBinaryCopyRow]
public readonly partial struct RowData
{
public static short ColumnCount => 5;

[Name("id")]
public required int Id { get; init; }

Expand All @@ -28,15 +24,6 @@ namespace benchmarks;
[Name("counter")]
public required int? Counter { get; init; }

public void BindMany(IPgBindable bindable)
{
bindable.Bind(Id);
bindable.Bind(Text);
bindable.Bind(CreationDate);
bindable.Bind(LastChangeDate);
bindable.Bind(Counter);
}

public override string ToString()
{
return
Expand Down
5 changes: 3 additions & 2 deletions benchmarks/benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../sqlx-cs-pg/build/Sqlx.Postgres.props" />

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -14,7 +16,6 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\sqlx-cs-core\sqlx-cs-core.csproj"/>
<ProjectReference Include="..\sqlx-cs-pg\sqlx-cs-pg.csproj"/>
<ProjectReference Include="..\sqlx-cs-pg-generator\sqlx-cs-pg-generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
Expand Down
73 changes: 73 additions & 0 deletions examples/Postgres.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// See https://aka.ms/new-console-template for more information

using Sqlx.Core.Pool;
using Sqlx.Postgres.Connection;
using Sqlx.Postgres.Generator;
using Sqlx.Postgres.Generator.Query;
using Sqlx.Postgres.Generator.Result;
using Sqlx.Postgres.Pool;

namespace examples;

public static class Postgres
{
public static async Task Run()
{
var username = Environment.GetEnvironmentVariable("PG_EXAMPLE_USERNAME")
?? throw new Exception("Could not find env variable");
var password = Environment.GetEnvironmentVariable("PG_EXAMPLE_PASSWORD")
?? throw new Exception("Could not find env variable");

var options = new PgConnectOptions
{
Host = "localhost",
Username = username,
Database = Environment.GetEnvironmentVariable("PG_EXAMPLE_DATABASE"),
Password = password,
};
PoolOptions poolOptions = new();

await using var pool = IPgConnectionPool.Create(options, poolOptions);

const string query =
"""
SELECT
s.value AS id, REPEAT('x', 2000) AS text_field,
current_timestamp AS creation_date, current_timestamp AS last_change_date,
NULL AS counter
FROM generate_series(1, $1) s(value);
""";

await foreach (Row row in pool.FetchAsync<Param, Row>(query, new Param { Count = 100 }))
{
Console.WriteLine(row);
}
}
}

[ToParam]
public readonly partial struct Param
{
public required int Count { get; init; }
}

[FromRow(RenameAll = Rename.SnakeCase)]
public readonly partial struct Row
{
public required int Id { get; init; }

[PgName("text_field")]
public required string Text { get; init; }

public required DateTime CreationDate { get; init; }

public required DateTime LastChangeDate { get; init; }

public required int? Counter { get; init; }

public override string ToString()
{
return
$"{nameof(Id)}: {Id}, {nameof(Text)}: {Text}, {nameof(CreationDate)}: {CreationDate}, {nameof(LastChangeDate)}: {LastChangeDate}, {nameof(Counter)}: {Counter}";
}
}
3 changes: 3 additions & 0 deletions examples/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using examples;

await Postgres.Run();
18 changes: 18 additions & 0 deletions examples/examples.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="../sqlx-cs-pg/build/Sqlx.Postgres.props" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\sqlx-cs-pg\sqlx-cs-pg.csproj"/>
<ProjectReference Include="..\sqlx-cs-pg-generator\sqlx-cs-pg-generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

</Project>
Loading
Loading