Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
107 changes: 9 additions & 98 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,87 +68,21 @@ jobs:
- name: Build solution
run: dotnet build ./src/NEventStore.Persistence.Sql.Core.sln -c Release --no-restore /p:ContinuousIntegrationBuild=True

# Tests run on Linux with database services (PostgreSQL, MySQL, SQLite).
# Each database provider runs independently to isolate failures and validate
# database-specific persistence logic.
test-sql-databases:
needs: build
name: Test (Linux, ${{ matrix.tfm }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Run each modern TFM independently so failures are isolated and easy to read.
tfm:
- net8.0
- net9.0
- net10.0

services:
sqlexpress:
image: mcr.microsoft.com/mssql/server:2022-RTM-GDR1-ubuntu-20.04
env:
SA_PASSWORD: Password12!
ACCEPT_EULA: Y
MSSQL_PID: Express
options: >-
--health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'Password12!' -Q 'SELECT 1'"
--health-interval 20s
--health-timeout 10s
--health-retries 10
--health-start-period 30s
ports:
- 1433:1433

postgresql:
image: postgres:15-alpine
env:
POSTGRES_DB: NEventStore
POSTGRES_USER: postgres
POSTGRES_PASSWORD: Password12!
options: >-
--health-cmd pg_isready
--health-interval 20s
--health-timeout 10s
--health-retries 10
--health-start-period 30s
ports:
- 5432:5432

mysql:
image: mysql:8
env:
MYSQL_DATABASE: NEventStore
MYSQL_ROOT_PASSWORD: Password12!
options: >-
--health-cmd "mysqladmin ping -h localhost"
--health-interval 20s
--health-timeout 10s
--health-retries 10
--health-start-period 30s
ports:
- 3306:3306

oracle:
image: gvenzl/oracle-xe
env:
ORACLE_ALLOW_REMOTE: true
ORACLE_PASSWORD: Password12!
options: >-
--health-cmd "sqlplus -L -S / as sysdba @/dev/null"
--health-interval 25s
--health-timeout 10s
--health-retries 10
--health-start-period 30s
ports:
- 1521:1521

env:
NEventStore.MsSql: Server=localhost;Database=NEventStore;User Id=SA;Password=Password12!;TrustServerCertificate=True;
NEventStore.PostgreSql: Server=localhost;Database=NEventStore;Uid=postgres;Pwd=Password12!;Enlist=false;
NEventStore.MySql: Server=localhost;Database=NEventStore;Uid=root;Pwd=Password12!;AutoEnlist=false;
NEVENTSTORE_ENVIRONMENT: ci
NEventStore.Sqlite: Data Source=:memory:;Cache=Shared;
NEventStore.Oracle: Data Source=localhost:1521/XE;User Id=system;Password=Password12!;Persist Security Info=True;

steps:
- name: Checkout
Expand All @@ -174,43 +108,20 @@ jobs:
restore-keys: |
nuget-${{ runner.os }}-

- name: Create NEventStore database once SQL Server is healthy
- name: Start isolated CI database environment
shell: bash
run: |
set -euo pipefail

# Find the SQL Server service container by ancestor image (more reliable in GH Actions)
container_id="$(/usr/bin/docker ps --filter "ancestor=mcr.microsoft.com/mssql/server:2022-RTM-GDR1-ubuntu-20.04" --format "{{.ID}}" | head -n 1)"
if [ -z "$container_id" ]; then
echo "SQL Server service container not found."
/usr/bin/docker ps --format "{{.ID}} {{.Image}} {{.Names}}" || true
exit 1
fi

# Wait until container reports healthy
for i in $(seq 1 60); do
health_status="$(/usr/bin/docker inspect --format='{{.State.Health.Status}}' "$container_id" 2>/dev/null || echo starting)"
if [ "$health_status" = "healthy" ]; then
echo "SQL Server container healthy"
break
fi
echo "Waiting for SQL Server to be healthy: $health_status ($i/60)"
sleep 2
done

if [ "$health_status" != "healthy" ]; then
echo "SQL Server container is not healthy. Logs follow (last 200 lines):"
/usr/bin/docker logs "$container_id" --tail 200 || true
exit 1
fi

# Create database using sqlcmd inside the container
/usr/bin/docker exec "$container_id" /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "Password12!" -Q "IF DB_ID(N'NEventStore') IS NULL CREATE DATABASE [NEventStore];"
run: bash ./start-environment.sh ci

- name: Run tests for ${{ matrix.tfm }}
run: dotnet test ./src/NEventStore.Persistence.Sql.Core.sln -c Release -f ${{ matrix.tfm }} --logger "trx;LogFileName=test-results-${{ matrix.tfm }}.trx"

- name: Stop CI database environment
if: always()
shell: bash
run: bash ./stop-environment.sh ci --remove-data

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-${{ matrix.tfm }}
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,10 @@ UpgradeLog*.XML

# Custom
artifacts/
.tokensave/
.tokensave/

# Generated local database environment configuration
.env.dynamic
.env.debug
.env.test
.env.ci
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ See [build.ps1](build.ps1), [appveyor.yml](appveyor.yml), and [README.md](README

Use [docker/Readme.md](docker/Readme.md) and [README.md](README.md) as the source of truth for environment setup.

## Isolated Database Environments

- Work only inside the assigned Git worktree.
- Use the repository `start-environment` and `stop-environment` scripts; use a dynamic environment by default.
- Do not use the reserved `debug`, `test`, or `ci` environments unless the task explicitly requires one.
- Never commit `.env.dynamic`, `.env.debug`, `.env.test`, or `.env.ci`.
- Never use global Docker cleanup commands.
- Do not stop, remove, inspect for mutation, or otherwise modify another worktree's Compose environment.

## Repository Map

- Core persistence library: `src/NEventStore.Persistence.Sql/`
Expand Down
56 changes: 27 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,30 @@ To build the project locally on a Windows Machine:
- Optional: update `.\src\.nuget\NEventStore.Persistence.Sql.nuspec` file if needed (before creating relase packages).
- Open a Powershell console in Administrative mode and run the build script `build.ps1` in the root of the repository.

## How to Run Unit Tests (locally)
## How to Run Tests (locally)

- Install Database engines or use Docker to run them in a container (you can use the scripts in `./docker` folder).
- Define the following environment variables:
Start the complete Linux-container database environment from the repository root. With no explicit name, the script derives a dynamic environment from the current Git worktree directory and Docker assigns collision-free host ports.

```
NEventStore.MsSql="Server=localhost,50001;Database=NEventStore;User Id=sa;Password=Password1;TrustServerCertificate=True;"
NEventStore.MySql="Server=localhost;Port=50003;Database=NEventStore;Uid=sa;Pwd=Password1;AutoEnlist=false;"
NEventStore.PostgreSql="Server=localhost;Port=50004;Database=NEventStore;Uid=sa;Pwd=Password1;Enlist=false;"
NEventStore.Oracle="Data Source=localhost:50005/XE;User Id=system;Password=Password1;Persist Security Info=True;"
```
```bash
./start-environment.sh
NEVENTSTORE_ENVIRONMENT=dynamic dotnet test ./src/NEventStore.Persistence.Sql.Core.sln
./stop-environment.sh
```

PowerShell equivalents are also provided:

```powershell
./start-environment.ps1
$env:NEVENTSTORE_ENVIRONMENT = 'dynamic'
dotnet test ./src/NEventStore.Persistence.Sql.Core.sln
./stop-environment.ps1
```

Reserved `debug`, `test`, and `ci` environments use predictable ports and may run concurrently. Explicit dynamic names such as `issue-142` are also supported. Normal stop preserves database volumes; pass `--remove-data` to the stop script to remove only the selected environment's data.

Successful starts generate one ignored local configuration file (`.env.dynamic`, `.env.debug`, `.env.test`, or `.env.ci`). Tests compose provider connection strings from that selected file. Existing complete connection-string environment variables remain supported and take precedence.

See [docker/Readme.md](docker/Readme.md) for commands, fixed ports, naming, generated values, and safety behavior.

## How to contribute

Expand All @@ -81,19 +94,15 @@ This repository uses GitFlow to develop, if you are not familiar with GitFlow yo

### Installing and configuring Git Flow

Probably the most straightforward way to install GitFlow on your machine is installing [Git Command Line](https://git-for-windows.github.io/), then install the [Visual Studio Plugin for Git-Flow](https://visualstudiogallery.msdn.microsoft.com/27f6d087-9b6f-46b0-b236-d72907b54683). This plugin is accessible from the **Team Explorer** menu and allows you to install GitFlow extension directly from Visual Studio with a simple click. The installer installs standard GitFlow extension both for command line and for Visual Studio Plugin.

Once installed you can use GitFlow right from Visual Studio or from Command line, which one you prefer.
Probably the most straightforward way to install GitFlow on your machine is installing [Git Command Line](https://git-for-windows.github.io/), then install the [Visual Studio Plugin for Git-Flow](https://visualstudiogallery.msdn.microsoft.com/vsgallery/27f6f087-9b6f-46b0-b236-d72907b54683). This plugin is accessible from the **Team Explorer** menu and allows you to install GitFlow extension directly for both command line and Visual Studio.

### Build machine and GitVersion

Build machine uses [GitVersion](https://github.com/GitTools/GitVersion) to manage automatic versioning of assemblies and Nuget Packages. You need to be aware that there are a rule that does not allow you to directly commit on master, or the build will fail.

A commit on master can be done only following the [Git-Flow](http://nvie.com/posts/a-successful-git-branching-model/) model, as a result of a new release coming from develop, or with an hotfix.
Build machine uses [GitVersion](https://github.com/GitTools/GitVersion) to manage automatic versioning of assemblies and Nuget Packages. Direct commits on master are not allowed by the build. A commit on master should only result from the repository's GitFlow release or hotfix process.

### Quick Info for NEventstore projects

Just clone the repository and from command line checkout develop branch with
Just clone the repository and from command line checkout develop branch with

```
git checkout develop
Expand All @@ -109,17 +118,6 @@ You can leave all values as default. Now your repository is GitFlow enabled.

### Note on Nuget version on Nuspec

Remember to update `.\src\.nuget\NEventStore.Persistence.Sql.nuspec` file if needed (before creating relase packages).

The .nuspec file is needed because the new `dotnet pack` command has problems dealing with ProjectReferences, submodules get the wrong version number.

While we are on develop branch, (suppose we just bumped major number so the driver version number is 6.0.0-unstablexxxx), we need to declare that this persistence driver depends from a version greater than the latest published. If the latest version of NEventStore 5.x.x wave iw 5.4.0 we need to declare this package dependency as

(5.4, 7)

This means, that we need a NEventStore greater than the latest published, but lesser than the next main version. This allows version 6.0.0-unstable of NEventStore to satisfy the dependency. We remember that prerelease package are considered minor than the stable package. Es.
Remember to update `.\src\.nuget\NEventStore.Persistence.Sql.nuspec` if needed before creating release packages.

5.4.0
5.4.1
6.0.0-unstable00001
6.0.0
The `.nuspec` file is needed because `dotnet pack` has problems dealing with ProjectReferences where submodules get the wrong version number.
24 changes: 0 additions & 24 deletions docker/DockerComposeDown.ps1

This file was deleted.

21 changes: 0 additions & 21 deletions docker/DockerComposeUp.ps1

This file was deleted.

87 changes: 76 additions & 11 deletions docker/Readme.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,83 @@
# Docker
# Docker database environments

Docker must be configured to use LCOW in Windows OS.
The repository starts SQL Server, MySQL, PostgreSQL, and Oracle as one Linux-container Docker Compose project. Each environment has isolated containers, networks, named volumes, and database data.

Run the following scripts to start and stop all the Database engines:
Run scripts from the repository root:

```bash
./start-environment.sh
./start-environment.sh debug
./start-environment.sh test
./start-environment.sh ci
./start-environment.sh issue-142

./stop-environment.sh
./stop-environment.sh debug
./stop-environment.sh issue-142
./stop-environment.sh issue-142 --remove-data
```

PowerShell uses the equivalent `start-environment.ps1` and `stop-environment.ps1` commands.

## Environment names

`debug`, `test`, and `ci` are reserved fixed environments. Any other name is dynamic. When no name is supplied, the script derives it from the Git worktree directory.

Names are converted to lowercase, unsupported characters become `-`, repeated separators collapse, and leading/trailing separators are removed. The Compose project name is:

```text
neventstore-<normalized-environment-name>
```

Compose generates all container, network, and volume names. No service uses `container_name`, because fixed names would prevent parallel worktrees.

## Ports

| Database | Debug | Test | CI |
| --- | ---: | ---: | ---: |
| SQL Server | 50001 | 51001 | 52001 |
| MySQL | 50003 | 51003 | 52003 |
| PostgreSQL | 50004 | 51004 | 52004 |
| Oracle | 50005 | 51005 | 52005 |

Fixed ports make human debugging and CI configuration predictable. Dynamic environments bind to `127.0.0.1` with Docker-assigned host ports so parallel worktrees do not need a shared allocator and cannot collide under normal Docker behavior.

## Generated configuration

A successful start overwrites exactly one local file after every database is healthy and SQL Server's `NEventStore` database exists:

- `debug` writes `.env.debug`
- `test` writes `.env.test`
- `ci` writes `.env.ci`
- any dynamic name writes `.env.dynamic`

The files contain provider components such as host, port, database, username, and password. Connection strings are assembled by the test code so shell scripts do not own provider syntax.

The existing complete connection-string environment variables remain supported and take precedence:

- `NEventStore.MsSql`
- `NEventStore.MySql`
- `NEventStore.PostgreSql`
- `NEventStore.Oracle`

When those variables are absent, tests load one selected file. Set `NEVENTSTORE_ENVIRONMENT` to `dynamic`, `debug`, `test`, or `ci`; it defaults to `dynamic`.

```bash
NEVENTSTORE_ENVIRONMENT=dynamic dotnet test ./src/NEventStore.Persistence.Sql.Core.sln
NEVENTSTORE_ENVIRONMENT=debug dotnet test ./src/NEventStore.Persistence.Sql.Core.sln
```
DockerComposeUp.ps1
DockerComposeDown.ps1

```powershell
$env:NEVENTSTORE_ENVIRONMENT = 'test'
dotnet test ./src/NEventStore.Persistence.Sql.Core.sln
```

## Docker Environment Variables
The generated files are ignored by Git and must not be committed.

## Data lifecycle and safety

Normal stop preserves named volumes and generated `.env` files. This makes repeated starts fast and keeps database state available for investigation.

`--remove-data` passes `--volumes` only to the selected Compose project. It does not remove another worktree's data. Global commands such as `docker system prune`, `docker volume prune`, and `docker network prune` are prohibited because they are not scoped to one environment.

NEventStore.MsSql="Server=localhost,50001;Database=NEventStore;User Id=sa;Password=Password1;TrustServerCertificate=True;"
NEventStore.MongoDB="mongodb://localhost:50002/NEventStore"
NEventStore.MySql="Server=localhost;Port=50003;Database=NEventStore;Uid=sa;Pwd=Password1;AutoEnlist=false;"
NEventStore.PostgreSql="Server=localhost;Port=50004;Database=NEventStore;Uid=sa;Pwd=Password1;Enlist=false;"
NEventStore.Oracle="Data Source=localhost:50005/XE;User Id=system;Password=Password1;Persist Security Info=True;"
The scripts are non-interactive and return a non-zero exit code when Docker, Compose, service health, SQL Server initialization, port discovery, or configuration-file generation fails.
Loading
Loading