Skip to content

Commit cbfbc06

Browse files
committed
Initial support of RepoDB Bulk Operations for AuroraDB via Npgsql. Ref: #1356
1 parent c431670 commit cbfbc06

59 files changed

Lines changed: 32239 additions & 15 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/ISSUE_TEMPLATE/report-a-bug.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ body:
6969
- RepoDb.MariaDbConnector.BulkOperations
7070
- RepoDb.Oracle.BulkOperations
7171
- RepoDb.Db2.BulkOperations
72+
- RepoDb.AuroraDb.PostgreSql.BulkOperations
7273
- RepoDb.ClickHouse.BulkOperations
7374
- RepoDb.CockroachDb.BulkOperations
7475
- RepoDb.DuckDb.BulkOperations
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Build & Run Tests - RepoDb.AuroraDb.PostgreSql.BulkOperations (Push)
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- 'src/Providers/RepoDb.Core/**'
8+
- 'src/Providers/RepoDb.AuroraDb.PostgreSql/**'
9+
- 'src/Providers/RepoDb.AuroraDb.PostgreSql.BulkOperations/**'
10+
- '.github/workflows/build-base.yml'
11+
- '.github/workflows/build-auroradb-postgresql-bulk.yml'
12+
13+
jobs:
14+
build:
15+
uses: ./.github/workflows/build-base.yml
16+
with:
17+
solution-dir: src/Providers/RepoDb.AuroraDb.PostgreSql.BulkOperations
18+
solution-file: RepoDb.AuroraDb.PostgreSql.BulkOperations.sln
19+
artifact-name: release-auroradb-postgresql-bulk
20+
db-engine: auroradbnpgsql

‎.github/workflows/build-base.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ env:
3939
# Amazon Aurora PostgreSQL (a plain PostgreSQL with PostGIS stands in for an Aurora PostgreSQL cluster)
4040
REPODB_AURORADB_CONSTR_SYSTEM: "Server=127.0.0.1;Port=5435;Database=postgres;User Id=postgres;Password=RepoDB2026;"
4141
REPODB_AURORADB_CONSTR: "Server=127.0.0.1;Port=5435;Database=RepoDb;User Id=postgres;Password=RepoDB2026;"
42+
REPODB_AURORADB_CONSTR_BULK: "Server=127.0.0.1;Port=5435;Database=RepoDbBulk;User Id=postgres;Password=RepoDB2026;"
4243
# MySQL
4344
REPODB_MYSQL_CONSTR_SYSTEM: "Server=127.0.0.1;Port=3306;Database=sys;User ID=root;Password=RepoDB2026;"
4445
REPODB_MYSQL_CONSTR: "Server=127.0.0.1;Port=3306;Database=RepoDb;User ID=root;Password=RepoDB2026;"
@@ -203,7 +204,7 @@ jobs:
203204
name: ${{ inputs.artifact-name }}
204205
path: release/*nupkg
205206

206-
# Amazon Aurora PostgreSQL only (e.g. RepoDb.AuroraDb.PostgreSql). A plain PostgreSQL with PostGIS stands in for an Aurora PostgreSQL cluster.
207+
# Amazon Aurora PostgreSQL only (e.g. RepoDb.AuroraDb.PostgreSql, RepoDb.AuroraDb.PostgreSql.BulkOperations). A plain PostgreSQL with PostGIS stands in for an Aurora PostgreSQL cluster.
207208
build-auroradbnpgsql:
208209
if: inputs.db-engine == 'auroradbnpgsql'
209210
runs-on: ubuntu-latest
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: PR - Build & Run Tests - RepoDb.AuroraDb.PostgreSql.BulkOperations
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- 'src/Providers/RepoDb.Core/**'
8+
- 'src/Providers/RepoDb.AuroraDb.PostgreSql/**'
9+
- 'src/Providers/RepoDb.AuroraDb.PostgreSql.BulkOperations/**'
10+
- '.github/workflows/build-base.yml'
11+
- '.github/workflows/build-pr-auroradb-postgresql-bulk.yml'
12+
13+
jobs:
14+
build:
15+
uses: ./.github/workflows/build-base.yml
16+
with:
17+
solution-dir: src/Providers/RepoDb.AuroraDb.PostgreSql.BulkOperations
18+
solution-file: RepoDb.AuroraDb.PostgreSql.BulkOperations.sln
19+
artifact-name: release-pr
20+
db-engine: auroradbnpgsql
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Release - RepoDb.AuroraDb.PostgreSql.BulkOperations (Manual)
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: "Package version to publish (e.g. 1.14.0)"
8+
required: true
9+
type: string
10+
dependency-overrides:
11+
description: "Semicolon-separated pins for dependencies not released at the same version this run, e.g. 'RepoDbCoreVersion=1.14.0;RepoDbAuroraDbPostgreSqlVersion=1.14.0'"
12+
required: false
13+
type: string
14+
default: ''
15+
release:
16+
types: [published]
17+
18+
jobs:
19+
publish:
20+
uses: ./.github/workflows/release-base.yml
21+
with:
22+
version: ${{ inputs.version }}
23+
dependency-overrides: ${{ inputs.dependency-overrides }}
24+
project-name: RepoDb.AuroraDb.PostgreSql.BulkOperations
25+
project-path: src/Providers/RepoDb.AuroraDb.PostgreSql.BulkOperations/RepoDb.AuroraDb.PostgreSql.BulkOperations/RepoDb.AuroraDb.PostgreSql.BulkOperations.csproj

‎LIMITATIONS.md‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ We want the .NET community to understand this library's limitations before using
118118
- [TransactionScope Fails on a Server That Is Not an Aurora Cluster](#transactionscope-fails-on-a-server-that-is-not-an-aurora-cluster)
119119
- [PostgreSQL Enumerations Need Npgsql's Global Type Mapper](#postgresql-enumerations-need-npgsqls-global-type-mapper)
120120
- [Spatial (PostGIS) Values Need an Npgsql Plugin](#spatial-postgis-values-need-an-npgsql-plugin)
121-
- [No Bulk Operations Package Yet](#no-bulk-operations-package-yet)
121+
- [Bulk Operations Staging Table](#bulk-operations-staging-table-10)
122+
- [Bulk Insert and Merge Identity Correlation Relies on RETURNING Order](#bulk-insert-and-merge-identity-correlation-relies-on-returning-order-2)
123+
- [Bulk COPY Errors Are Not Wrapped in AuroraDbException](#bulk-copy-errors-are-not-wrapped-in-auroradbexception)
124+
- [Bulk Operations Ignore a Completed Transaction](#bulk-operations-ignore-a-completed-transaction)
122125
- [Verification Status](#verification-status-10)
123126

124127
## Core
@@ -1618,7 +1621,7 @@ This could not be tested, because the staging tables these statements read from
16181621

16191622
## AuroraDB (PostgreSQL)
16201623

1621-
These limitations are specific to the `RepoDb.AuroraDb.PostgreSql` package, on top of the [Core](#core) limitations above. Amazon Aurora PostgreSQL is PostgreSQL-compatible, so the package generates the same SQL as `RepoDb.PostgreSql`: `"` quoting, `RETURNING` for identities, `INSERT ... ON CONFLICT` for merges, `LIMIT`/`OFFSET` for paging, and `TRUNCATE ... RESTART IDENTITY`. It is built on [`RepoDb.Connector.AuroraDb.Npgsql`](https://www.nuget.org/packages/RepoDb.Connector.AuroraDb.Npgsql). That connector's `AuroraDbConnection` wraps Npgsql in the [AWS Advanced .NET Data Provider Wrapper](https://github.com/aws/aws-advanced-dotnet-data-provider-wrapper) (`AwsWrapperConnection<NpgsqlConnection>`). Unless stated otherwise, each behavior below was observed by running the integration test suite against the `postgis/postgis:latest` container (PostgreSQL `17.5`, PostGIS `3.5.2`). That container is a plain PostgreSQL server standing in for an Aurora cluster. None of it has been run against a real Aurora cluster.
1624+
These limitations are specific to the `RepoDb.AuroraDb.PostgreSql` and `RepoDb.AuroraDb.PostgreSql.BulkOperations` packages, on top of the [Core](#core) limitations above. Amazon Aurora PostgreSQL is PostgreSQL-compatible, so the package generates the same SQL as `RepoDb.PostgreSql`: `"` quoting, `RETURNING` for identities, `INSERT ... ON CONFLICT` for merges, `LIMIT`/`OFFSET` for paging, and `TRUNCATE ... RESTART IDENTITY`. Both are built on [`RepoDb.Connector.AuroraDb.Npgsql`](https://www.nuget.org/packages/RepoDb.Connector.AuroraDb.Npgsql), and the bulk operations write rows through its `AuroraDbBulkCopy` (Npgsql binary `COPY`). That connector's `AuroraDbConnection` wraps Npgsql in the [AWS Advanced .NET Data Provider Wrapper](https://github.com/aws/aws-advanced-dotnet-data-provider-wrapper) (`AwsWrapperConnection<NpgsqlConnection>`). Unless stated otherwise, each behavior below was observed by running the integration test suite against the `postgis/postgis:latest` container (PostgreSQL `17.5`, PostGIS `3.5.2`). That container is a plain PostgreSQL server standing in for an Aurora cluster. None of it has been run against a real Aurora cluster.
16221625

16231626
### RepoDb.Connector.AuroraDb.Npgsql Is a Prerelease Package
16241627

@@ -1658,7 +1661,7 @@ None within RepoDB. The probes come from the AWS wrapper inside the connector. O
16581661

16591662
Inside a `TransactionScope`, the connection is enlisted in the ambient transaction when it opens. The failed probe from the [previous section](#the-aws-wrapper-probes-for-aurora-on-every-connection-open) then runs inside that transaction and aborts it. The first RepoDB operation fails with an `AuroraDbException` with `SqlState` `25P02` (`current transaction is aborted, commands ignored until end of transaction block`), and nothing is committed. Explicit transactions (`connection.BeginTransaction()`) are not affected and pass their tests.
16601663

1661-
On an Aurora cluster the probe succeeds, so `TransactionScope` is expected to work there, but this has not been verified. In the integration suite, the six `TransactionScope` tests are reported as inconclusive (skipped) when the server is not an Aurora cluster. Two negative tests pin the `25P02` failure, so a fix in the connector or the AWS wrapper will show up as a test failure.
1664+
On an Aurora cluster the probe succeeds, so `TransactionScope` is expected to work there, but this has not been verified. In the integration suite, the six `TransactionScope` tests are reported as inconclusive (skipped) when the server is not an Aurora cluster. Two negative tests pin the `25P02` failure, so a fix in the connector or the AWS wrapper will show up as a test failure. The bulk operations behave the same way, and their suite has one inconclusive and one negative test of its own.
16621665

16631666
**Alternative Solution**
16641667

@@ -1685,14 +1688,44 @@ Aurora PostgreSQL supports PostGIS, and `AuroraDbType` has `Geometry` and `Geogr
16851688

16861689
Exchange the values as Well-Known Text through the PostGIS functions: `ST_GeomFromText(@Wkt, 4326)`/`ST_GeogFromText(@Wkt)` when writing, and `ST_AsText("Column")` when reading, with `ExecuteQuery`/`ExecuteScalar`. The integration suite's `SpatialTests` cover this. Registering `Npgsql.NetTopologySuite` through the global type mapper has not been tested.
16871690

1688-
### No Bulk Operations Package Yet
1691+
### Bulk Operations Staging Table
1692+
1693+
`BulkMerge`, `BulkUpdate`, `BulkDelete`, `BulkDeleteByKey`, and `BulkInsert` with `identityBehavior: ReturnIdentity` all stage rows into a pseudo table before applying them to the target table. Only a plain `BulkInsert` (without `ReturnIdentity`) writes straight into the target table through `AuroraDbBulkCopy`. The staging SQL is the same as [EnterpriseDB](#bulk-operations-staging-table-7)'s, and runs unchanged on PostgreSQL:
1694+
1695+
- **Staging table creation.** `CREATE [TEMP] TABLE <pseudo> AS SELECT ... WHERE (1 = 0)`, then `ALTER TABLE <pseudo> ADD COLUMN "__RepoDbBulkRowOrder__" BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY`, then `TRUNCATE TABLE <pseudo> RESTART IDENTITY`. These are DDL statements, so they need the `CREATE` privilege on the schema (for `Physical` staging) or the `TEMPORARY` privilege on the database (for `Memory` staging).
1696+
- **Staging table type.** `Memory` uses a session-private `TEMP` table. `Physical` uses an ordinary table in the target schema, and `Auto` (the default) switches from `Memory` to `Physical` at 5,000 rows. A staging table is dropped once the operation finishes, including when it runs inside a transaction that is later rolled back.
1697+
- **Name collisions.** Staging tables are named deterministically from `{pseudoTableType}{tableName}{Operation}` (for example `PhysicalPersonMerge`), the same scheme as the other bulk packages in this document. Two concurrent callers using `Physical` staging (or `Auto` at 5,000 rows or more) against the same target table share one staging table and can corrupt each other's staged rows.
1698+
1699+
**Alternative Solution**
16891700

1690-
There is no `RepoDb.AuroraDb.PostgreSql.BulkOperations` package yet (`src/Providers/RepoDb.AuroraDb.PostgreSql.BulkOperations` only holds a placeholder README). The connector already ships `AuroraDbBulkCopy` (Npgsql binary `COPY`) for a future add-on.
1701+
Use `Memory` staging for concurrent callers, and make sure the connecting user has the privileges above. On an Aurora cluster with read replicas, send bulk operations to the writer (cluster) endpoint, since they run DDL and write statements.
1702+
1703+
### Bulk Insert and Merge Identity Correlation Relies on RETURNING Order
1704+
1705+
`BulkInsert`/`BulkMerge` with `identityBehavior: ReturnIdentity` read identity values back with a single `INSERT ... SELECT ... ORDER BY <row-order column> ... RETURNING <identity>` statement. Each returned value is then assigned to the source entity or `DataRow` in order, which assumes `RETURNING` emits rows in the same order as the ordered `SELECT`. This is the same assumption described for [EnterpriseDB](#bulk-insert-and-merge-identity-correlation-relies-on-returning-order). PostgreSQL doesn't document any guarantee about `RETURNING` row order.
16911706

16921707
**Alternative Solution**
16931708

1694-
Use the batched `InsertAll`/`MergeAll`/`UpdateAll` operations, or `AuroraDbBulkCopy` from `RepoDb.Connector.AuroraDb.Npgsql` directly.
1709+
On a single PostgreSQL server, the integration suite checks the correlation for 1,000 rows (sync `BulkInsert` and async `BulkMerge`) by comparing each returned identity with the `RowGuid` of its row, and it holds. It has not been checked on an Aurora cluster. For critical data, verify the correlation against your cluster, or use a natural key instead of `ReturnIdentity`.
1710+
1711+
### Bulk COPY Errors Are Not Wrapped in AuroraDbException
1712+
1713+
`AuroraDbBulkCopy` runs the binary `COPY` on the Npgsql connection it unwraps from the AWS wrapper, so a server error raised during the `COPY` itself surfaces as an `Npgsql.PostgresException`. Errors raised by the staged SQL statements (which go through `AuroraDbCommand`) surface as an `AuroraDbException`, and so do the errors of `RepoDb.AuroraDb.PostgreSql`. For example, a plain `BulkInsert` that violates a primary key (`23505`), a `NOT NULL` constraint (`23502`) or a `VARCHAR` length (`22001`) throws `Npgsql.PostgresException`. A `catch (AuroraDbException)` block does not catch it.
1714+
1715+
**Alternative Solution**
1716+
1717+
Catch `System.Data.Common.DbException`, which both exception types derive from, and read its `SqlState`. The integration suite asserts the errors this way. A lasting fix belongs in the connector: wrap the `COPY` errors of `AuroraDbBulkCopy` the same way as the command errors.
1718+
1719+
### Bulk Operations Ignore a Completed Transaction
1720+
1721+
When a transaction that has already been committed or rolled back is passed as the `transaction` argument, the bulk operations do not reject it. The rows are written outside of any transaction and are auto-committed, so they cannot be rolled back. This was observed for both a plain `BulkInsert` (binary `COPY`, which takes no transaction object at all) and a staged `BulkMerge`. `AuroraDbTransaction` does not expose whether it has completed, so the bulk operations cannot detect it.
1722+
1723+
**Alternative Solution**
1724+
1725+
Only pass a transaction that is still active, typically by keeping the bulk call inside the `using` block of `BeginTransaction()` and before `Commit()`. A lasting fix needs the connector to expose the completion state of `AuroraDbTransaction` (for example an `IsCompleted` property), so that the bulk operations can throw an `InvalidOperationException`. Two tests in the bulk suite pin the current behavior.
16951726

16961727
### Verification Status
16971728

16981729
`RepoDb.AuroraDb.PostgreSql.UnitTests` has 194 test methods (288 test cases once the data rows are counted), and all of them pass. `RepoDb.AuroraDb.PostgreSql.IntegrationTests` has 760 test methods, 118 of which are negative (`ThrowException...`). The unit tests were ported from `RepoDb.CockroachDb`. The integration tests were ported from `RepoDb.PostgreSql`, plus the `AuroraDbType` attribute tests from `RepoDb.CockroachDb` and new `SpatialTests`. For comparison, the original `RepoDb.PostgreSql` integration suite passes 746/746 against the same container. Against the `postgis/postgis:latest` container, 1,042 of the 1,048 test cases pass on each of `net8.0`, `net9.0`, and `net10.0`; the other 6 are the inconclusive `TransactionScope` tests described above. The `build-auroradb-postgresql`/`build-pr-auroradb-postgresql` CI workflows run both suites against a `postgis/postgis:latest` service container. That image tag is not pinned, so a new PostgreSQL or PostGIS release can change CI results without any code change.
1730+
1731+
`RepoDb.AuroraDb.PostgreSql.BulkOperations` was ported from `RepoDb.CockroachDb.BulkOperations`, with its CockroachDB-specific staging SQL reverted to the PostgreSQL SQL of `RepoDb.EnterpriseDb.BulkOperations`. Its integration suite (`RepoDb.AuroraDb.PostgreSql.BulkOperations.IntegrationTests`, in a `RepoDbBulk` database) has 629 test cases: the 590 carried over from `RepoDb.CockroachDb.BulkOperations` (441 positive, 149 negative), plus 39 scenario cases. The scenario cases cover transactions, the pseudo-table types (including the 5,000-row `Auto` threshold), identity correlation, batch sizes, partial matches, nulls, Unicode, type coercion, the repositories, `TransactionScope`, server errors and cancellation. Against the `postgis/postgis:latest` container, 628 of the 629 cases pass on each of `net8.0`, `net9.0`, and `net10.0`; the other one is the `TransactionScope` test that needs an Aurora cluster. There is no unit test project for the `AuroraDbText` SQL generation. The `build-auroradb-postgresql-bulk`/`build-pr-auroradb-postgresql-bulk` CI workflows run the suite against the same `postgis/postgis:latest` service container.

‎PACKAGES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Below are the complete list of provider-specific packages for each supported dat
3535

3636
| Project | Nuget | Downloads | Status |
3737
|---------|-------|-----------|--------|
38+
| [AuroraDB (PostgreSQL) (Bulk)](https://www.nuget.org/packages/RepoDb.AuroraDb.PostgreSql.BulkOperations) | [![](https://img.shields.io/nuget/v/RepoDb.AuroraDb.PostgreSql.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.AuroraDb.PostgreSql.BulkOperations) | [![](https://img.shields.io/nuget/dt/RepoDb.AuroraDb.PostgreSql.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.AuroraDb.PostgreSql.BulkOperations) | [![Build](https://img.shields.io/github/actions/workflow/status/mikependon/RepoDB/build-auroradb-postgresql-bulk.yml?logo=github&label=build&style=for-the-badge)](https://github.com/mikependon/RepoDB/actions/workflows/build-auroradb-postgresql-bulk.yml) |
3839
| [ClickHouse (Bulk)](https://www.nuget.org/packages/RepoDb.ClickHouse.BulkOperations) | [![](https://img.shields.io/nuget/v/RepoDb.ClickHouse.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.ClickHouse.BulkOperations) | [![](https://img.shields.io/nuget/dt/RepoDb.ClickHouse.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.ClickHouse.BulkOperations) | [![Build](https://img.shields.io/github/actions/workflow/status/mikependon/RepoDB/build-clickhouse-bulk.yml?logo=github&label=build&style=for-the-badge)](https://github.com/mikependon/RepoDB/actions/workflows/build-clickhouse-bulk.yml) |
3940
| [CockroachDB (Bulk)](https://www.nuget.org/packages/RepoDb.CockroachDb.BulkOperations) | [![](https://img.shields.io/nuget/v/RepoDb.CockroachDb.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.CockroachDb.BulkOperations) | [![](https://img.shields.io/nuget/dt/RepoDb.CockroachDb.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.CockroachDb.BulkOperations) | [![Build](https://img.shields.io/github/actions/workflow/status/mikependon/RepoDB/build-cockroachdb-bulk.yml?logo=github&label=build&style=for-the-badge)](https://github.com/mikependon/RepoDB/actions/workflows/build-cockroachdb-bulk.yml) |
4041
| [DB2 (Bulk)](https://www.nuget.org/packages/RepoDb.Db2.BulkOperations) | [![](https://img.shields.io/nuget/v/RepoDb.Db2.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.Db2.BulkOperations) | [![](https://img.shields.io/nuget/dt/RepoDb.Db2.BulkOperations?&logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/RepoDb.Db2.BulkOperations) | [![Build](https://img.shields.io/github/actions/workflow/status/mikependon/RepoDB/build-db2-bulk.yml?logo=github&label=build&style=for-the-badge)](https://github.com/mikependon/RepoDB/actions/workflows/build-db2-bulk.yml) |

0 commit comments

Comments
 (0)