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/.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 a72b156..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 @@ -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 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/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); + } +} 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 + + + + +