From 27059c668bccd53ed41acee5767f6256c89a72c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Amorim?= Date: Sat, 11 Jul 2026 02:47:31 +0100 Subject: [PATCH 1/4] Document the IntegrationTests project convention M3 introduces the first integration test project, needing a real dependency (a database provider) rather than hand-rolled fakes. Adds test/.IntegrationTests/ alongside .UnitTests/ in architecture.md, and broadens Directory.Build.props' test-project condition from EndsWith('.UnitTests') to EndsWith('Tests') so the XML-doc exemption and IsPackable=false apply to it too, without needing another edit for the next test-suffixed project. --- .claude/rules/architecture.md | 2 +- Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 7a20da3..b8a203e 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -2,6 +2,6 @@ Rules governing physical project and solution structure (as opposed to logical design, which lives in `design-principles.md`): -- Repo/solution layout: source under `src//`, tests under `test/.UnitTests/` (singular `test`), one solution file (`.slnx`) per repo or example, at its root, referencing every project beneath it. +- Repo/solution layout: source under `src//`, unit tests under `test/.UnitTests/` (singular `test`), integration tests requiring a real dependency (e.g. a database provider) under `test/.IntegrationTests/`, one solution file (`.slnx`) per repo or example, at its root, referencing every project beneath it. - Centralize shared MSBuild properties (`TargetFramework`, `Nullable`, `TreatWarningsAsErrors`, etc.) in a `Directory.Build.props` at that same root instead of repeating them per `.csproj`. - `dotnet pack` runs in CI from day one so packaging bugs surface early. diff --git a/Directory.Build.props b/Directory.Build.props index a72b156..62dcef8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,7 +29,7 @@ as a build-time diagnostic (https://github.com/dotnet/roslyn/issues/41640); only the missing-doc- comment warning (CS1591) itself is suppressed. --> - + $(NoWarn);CS1591 false From d9d43a8b88e53e7d971606b1b58f3531ec97def8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Amorim?= Date: Sat, 11 Jul 2026 02:51:11 +0100 Subject: [PATCH 2/4] Scaffold SqlBound.IntegrationTests The first integration test project, referencing SqlBound, Dapper, and Microsoft.Data.Sqlite so the coexistence proof can run against a real ADO.NET provider instead of the hand-rolled fakes used in the unit tests. SQLite over SQL Server here: the coexistence proof only depends on the shared ADO.NET abstraction, not on any SQL-Server-specific behavior (that's M7's job), and SQLite needs no external service or CI setup. Pins SQLitePCLRaw.lib.e_sqlite3 to 3.53.3 directly: Microsoft.Data.Sqlite 10.0.9 transitively pulls 2.1.11, which carries a known high-severity vulnerability (CVE-2025-6965, a SQLite memory-corruption issue fixed upstream in SQLite 3.50.2) that NuGet's audit now fails restore on. --- sqlbound.slnx | 1 + .../SqlBound.IntegrationTests.csproj | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 test/SqlBound.IntegrationTests/SqlBound.IntegrationTests.csproj diff --git a/sqlbound.slnx b/sqlbound.slnx index 350349d..83892d5 100644 --- a/sqlbound.slnx +++ b/sqlbound.slnx @@ -3,6 +3,7 @@ + diff --git a/test/SqlBound.IntegrationTests/SqlBound.IntegrationTests.csproj b/test/SqlBound.IntegrationTests/SqlBound.IntegrationTests.csproj new file mode 100644 index 0000000..2bf32a9 --- /dev/null +++ b/test/SqlBound.IntegrationTests/SqlBound.IntegrationTests.csproj @@ -0,0 +1,32 @@ + + + + + net10.0 + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + From 99f0d6dedd3e6d8d23f865eefe018fe0971ed3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Amorim?= Date: Sat, 11 Jul 2026 02:54:16 +0100 Subject: [PATCH 3/4] Prove Dapper and SqlBound coexist on a shared connection and transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three scenarios against a real SQLite provider, each opening its own isolated in-memory connection: - interleaving SqlSession and Dapper calls on the same open connection - a transaction shared between both libraries: commit persists both libraries' writes - the same shared transaction: rollback discards both libraries' writes The rollback case is the strongest proof of genuine sharing — if either library weren't truly enlisted in the same transaction, its write would survive the rollback. Both namespaces (Dapper, SqlBound) are imported in the same file with zero method-name ambiguity, proven by compiling rather than by a runtime assertion, since SqlBound deliberately never defines Query*/Execute*/ExecuteScalar* extensions on DbConnection. All three passed on the first run against the real provider — SqlSession was previously verified only against hand-rolled fakes (M2), and this is the first confirmation its parameter binding and transaction handling work correctly against a live ADO.NET provider, not just idealized fake behavior. This project (and these tests) stay in the suite permanently so a coexistence regression fails CI. --- .../DapperCoexistenceTests.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/SqlBound.IntegrationTests/DapperCoexistenceTests.cs diff --git a/test/SqlBound.IntegrationTests/DapperCoexistenceTests.cs b/test/SqlBound.IntegrationTests/DapperCoexistenceTests.cs new file mode 100644 index 0000000..d518321 --- /dev/null +++ b/test/SqlBound.IntegrationTests/DapperCoexistenceTests.cs @@ -0,0 +1,101 @@ +using Dapper; +using Microsoft.Data.Sqlite; + +namespace SqlBound.IntegrationTests; + +public class DapperCoexistenceTests +{ + [Fact] + public async Task Should_ProduceConsistentResults_When_InterleavingSqlBoundAndDapperOnSameConnection() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(TestContext.Current.CancellationToken); + var session = new SqlSession(connection); + + await session.RunAsync( + "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", + cancellationToken: TestContext.Current.CancellationToken); + + await session.RunAsync( + "INSERT INTO items (id, name) VALUES (@id, @name)", + new SqlParameters(("@id", 1), ("@name", "from-sqlbound")), + TestContext.Current.CancellationToken); + + var readByDapper = await connection.QuerySingleAsync( + "SELECT name FROM items WHERE id = @id", new { id = 1 }); + Assert.Equal("from-sqlbound", readByDapper); + + await connection.ExecuteAsync( + "INSERT INTO items (id, name) VALUES (@id, @name)", new { id = 2, name = "from-dapper" }); + + var readBySqlBound = await session.FetchScalarAsync( + "SELECT name FROM items WHERE id = @id", + new SqlParameters(("@id", 2)), + TestContext.Current.CancellationToken); + Assert.Equal("from-dapper", readBySqlBound); + } + + [Fact] + public async Task Should_PersistBothWrites_When_SharedTransactionCommits() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(TestContext.Current.CancellationToken); + var session = new SqlSession(connection); + + await session.RunAsync( + "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", + cancellationToken: TestContext.Current.CancellationToken); + + await using (var transaction = await connection.BeginTransactionAsync(TestContext.Current.CancellationToken)) + { + var transactionalSession = new SqlSession(connection, transaction); + + await transactionalSession.RunAsync( + "INSERT INTO items (id, name) VALUES (@id, @name)", + new SqlParameters(("@id", 1), ("@name", "from-sqlbound")), + TestContext.Current.CancellationToken); + + await connection.ExecuteAsync( + "INSERT INTO items (id, name) VALUES (@id, @name)", + new { id = 2, name = "from-dapper" }, + transaction); + + await transaction.CommitAsync(TestContext.Current.CancellationToken); + } + + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM items"); + Assert.Equal(2, count); + } + + [Fact] + public async Task Should_DiscardBothWrites_When_SharedTransactionRollsBack() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(TestContext.Current.CancellationToken); + var session = new SqlSession(connection); + + await session.RunAsync( + "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", + cancellationToken: TestContext.Current.CancellationToken); + + await using (var transaction = await connection.BeginTransactionAsync(TestContext.Current.CancellationToken)) + { + var transactionalSession = new SqlSession(connection, transaction); + + await transactionalSession.RunAsync( + "INSERT INTO items (id, name) VALUES (@id, @name)", + new SqlParameters(("@id", 1), ("@name", "from-sqlbound")), + TestContext.Current.CancellationToken); + + await connection.ExecuteAsync( + "INSERT INTO items (id, name) VALUES (@id, @name)", + new { id = 2, name = "from-dapper" }, + transaction); + + await transaction.RollbackAsync(TestContext.Current.CancellationToken); + } + + var count = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM items"); + Assert.Equal(0, count); + } +} From 53b364aa3eaba322bb46c562d1fc753b616ef830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Amorim?= Date: Sat, 11 Jul 2026 02:55:32 +0100 Subject: [PATCH 4/4] Close Phase 1: drop the preview suffix to PackageVersion 0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps PackageVersion from 0.1.0-preview.1 to the clean 0.1.0 so the v0.1.0 tag applied to this commit after merge matches the version it actually marks — a tag should name the exact package a revision produces. Dropping the suffix doesn't overclaim stability: every 0.x version is already "anything may change" under semver, so 0.1.0 promises no more than 0.1.0-preview.1 did. Records the convention in workflow.md: PackageVersion carries a prerelease suffix during a phase's active development, and closing the phase drops it in the same commit that gets tagged. The next phase's first commit starts the new prerelease line. --- .claude/rules/workflow.md | 1 + Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/rules/workflow.md b/.claude/rules/workflow.md index 026e83c..793a9d6 100644 --- a/.claude/rules/workflow.md +++ b/.claude/rules/workflow.md @@ -10,5 +10,6 @@ - Merge by squash so each feature arrives as a single logical commit on the default branch, consistent with the "one logical change per commit" rule above; the squash commit message keeps the imperative, *why*-focused form. - Versioning follows semantic versioning: each phase gets its own minor version (Phase 1 → `0.1.x`, Phase 2 → `0.2.x`, …; Phase 6 ships `1.0.0`). - When a phase completes, tag it on the default branch with an annotated tag (e.g., `git tag -a v0.1.0 -m "..."`) and push the tag to GitHub for reference. +- `PackageVersion` carries a prerelease suffix (`X.Y.0-preview.N`) during a phase's active development. Closing the phase drops the suffix to the clean `X.Y.0` in the same commit that gets tagged, so the tag always matches the package version it marks exactly. The next phase's first commit starts the new prerelease line (`X.(Y+1).0-preview.1`). - Each roadmap milestone has a matching GitHub milestone (created per phase); the milestone's PR is associated on creation and the milestone closed on merge. - Update this file when a new convention or correction is established. diff --git a/Directory.Build.props b/Directory.Build.props index 62dcef8..26c121d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ Luís Amorim - 0.1.0-preview.1 + 0.1.0 Apache-2.0 https://github.com/lgamorim/sqlbound https://github.com/lgamorim/sqlbound