diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..4259fb2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Docs + +# Builds the DocFX documentation site and deploys it to GitHub Pages on every push +# to master. Requires GitHub Pages to be enabled for the repository with the source +# set to "GitHub Actions" (Settings -> Pages); until then the build still runs but +# the deploy step has nothing to publish to. +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; a newer push supersedes an in-progress run. +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Install DocFX + run: dotnet tool install --global docfx + + - name: Build site + run: docfx docfx.json + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9ae4d84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Release + +# Builds and packs the release on any v*.*.* tag, but never publishes on its own. +# Publishing to nuget.org happens only through a deliberate manual run of this +# workflow with publish=true and an expected-version that matches what the selected +# ref packs (run it from the release tag, not master) — and only once a maintainer +# has added the NUGET_API_KEY repository secret. A plain tag push produces verified +# packages as build artifacts for review; it does not release them. +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + publish: + description: 'Publish packages to nuget.org (leave unchecked to only build and pack)' + type: boolean + default: false + expected-version: + description: 'The exact PackageVersion this run must publish (e.g. 1.0.0); required when publish is checked' + type: string + default: '' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --configuration Release --no-build + + - name: Pack + run: dotnet pack --configuration Release --no-build --output artifacts + + - name: Upload NuGet artifacts + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + artifacts/*.nupkg + artifacts/*.snupkg + + - name: Verify the version being published + # A workflow_dispatch run builds whatever ref was selected in the "Run workflow" + # dropdown — master by default, not the release tag. Requiring the maintainer to + # type the exact version and matching it against the packed output makes publishing + # a stale or unintended ref fail here instead of reaching nuget.org. + if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }} + run: | + expected='${{ inputs.expected-version }}' + if [ -z "$expected" ]; then + echo "::error::publish=true requires expected-version to be set." + exit 1 + fi + if [ ! -f "artifacts/SqlBound.${expected}.nupkg" ]; then + echo "::error::This run packed $(ls artifacts/SqlBound.[0-9]*.nupkg), not version ${expected}. Select the release tag as the run's ref and set expected-version to its version." + exit 1 + fi + + - name: Publish to nuget.org + # Gate: only a manual workflow_dispatch with publish=true reaches this step. + # Tag pushes stop after packing. Publishing 1.0.0 is an opt-in maintainer action. + if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }} + run: > + dotnet nuget push "artifacts/*.nupkg" + --api-key "${{ secrets.NUGET_API_KEY }}" + --source https://api.nuget.org/v3/index.json + --skip-duplicate diff --git a/.gitignore b/.gitignore index 80fa0fa..6fe254f 100644 --- a/.gitignore +++ b/.gitignore @@ -430,3 +430,7 @@ FodyWeavers.xsd # JetBrains Rider .idea/ + +# DocFX documentation site (generated) +_site/ +api/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..eced30b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,124 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0-rc.1] - 2026-07-12 + +First release candidate for 1.0 (Phase 6 — Ship, M16). Freezes the public API +and finalizes packaging ahead of the stable 1.0.0 release. No functional +changes to the query, verification, or migration engines from `0.5.0` beyond +the MySQL scanner fix below. + +### Added + +- Strong-named assemblies across the whole library set, sharing one committed + key so strong-named consumers can reference SqlBound. +- Public-API baselines (`PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt`) + on every shipping library, enforced at build time by + `Microsoft.CodeAnalysis.PublicApiAnalyzers`, so post-1.0 surface changes are + caught mechanically. +- A package-oriented NuGet README embedded in every package, plus per-package + `PackageTags`. +- `ContinuousIntegrationBuild` for deterministic, path-normalized CI output; + SourceLink (SDK-provided) verified to map symbols to the committed sources. +- Guide: "Using SqlBound alongside Dapper" (`docs/dapper-coexistence.md`). +- ADR 0008: public-API stability policy, package-signing rationale, and the + documentation-site decision. +- A DocFX documentation site published to GitHub Pages. +- A tag-triggered NuGet release workflow (publish deliberately gated off until + the 1.0.0 promotion, and additionally guarded by a required + `expected-version` input that must match the packed output). + +### Fixed + +- The MySQL parameter scanner no longer mistakes `@@` system variables (e.g. + `@@sql_mode`, `@@session.sql_mode`) for parameter placeholders, which made + `prepare` declare a bogus parameter and verification demand a method + parameter that should not exist. +- The MySQL parameter scanner now applies MySQL's actual line-comment rule: + `--` starts a comment only when followed by whitespace or the end of the + statement, so an expression like `1--@x` no longer swallows the rest of the + line. +- `SqlSession.FetchScalarAsync` reports a failed scalar conversion as an + `InvalidOperationException` naming the actual and requested types (instead + of leaking a bare `InvalidCastException`), and its no-value error message + now acknowledges that the result may have been a database NULL. +- `sqlbound database drop` for SQLite also removes stale `-wal`/`-shm` + sidecar files a crashed process may have left beside the database file. + +### Changed + +- Narrowed the `SqlBound.Migrations` public surface: `MigrationPlan`, + `MigrationReverter`, and `MigrationStatusReport` are now `internal` (pure + decision helpers composed by `MigrationRunner`; widening later is + non-breaking, narrowing after 1.0 would not be). + +## [0.5.0] - 2026-07-12 + +Phase 5 — Migrations & CLI (M13–M15). + +### Added + +- SQL-file migration model (paired `{version}_{name}.up.sql` / `.down.sql`, + timestamp versions, SHA-256 checksums) and the `_sqlbound_migrations` ledger. +- Provider-neutral migration engine (`migrate add` / `run` / `revert` / + `status`) with checksum-drift and out-of-order safety checks, plus per-migration + transactions where the provider supports transactional DDL. +- `database create` / `drop` CLI commands. +- Cross-provider support for migrations and database administration across SQL + Server, SQLite, PostgreSQL, and MySQL. +- ADR 0006 (migration file format) and ADR 0007 (MySQL non-transactional DDL). + +## [0.4.0] - 2026-07-12 + +Phase 4 — Providers (M10–M12). + +### Added + +- Introspection providers for SQLite, PostgreSQL, and MySQL alongside SQL + Server, each with real type mapping and describe fidelity verified against a + live database. +- The `SqlBound.Introspection` abstraction (`IQueryDescriber`) that all + providers implement. + +## [0.3.0] - 2026-07-11 + +Phase 3 — Verification (M7–M9). + +### Added + +- SQL Server schema introspection, `SQLB###` diagnostics, and committed offline + JSON snapshots (`.sqlbound/`) so the in-IDE analyzer validates without a + database connection. + +## [0.2.0] - 2026-07-11 + +Phase 2 — Codegen (M4–M6). + +### Added + +- Reflection-free row materialization via an incremental source generator, + query-shape support, Native AOT compatibility, and benchmarks versus Dapper + and raw ADO.NET. + +## [0.1.0] - 2026-07-11 + +Phase 1 — Bedrock (M1–M3). + +### Added + +- Solution skeleton and CI, the `System.Data.Common`-based execution core, and + the Dapper-coexistence sample that doubles as a permanent CI regression test. + +[Unreleased]: https://github.com/lgamorim/sqlbound/compare/v1.0.0-rc.1...HEAD +[1.0.0-rc.1]: https://github.com/lgamorim/sqlbound/compare/v0.5.0...v1.0.0-rc.1 +[0.5.0]: https://github.com/lgamorim/sqlbound/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/lgamorim/sqlbound/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/lgamorim/sqlbound/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/lgamorim/sqlbound/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/lgamorim/sqlbound/releases/tag/v0.1.0 diff --git a/Directory.Build.props b/Directory.Build.props index 696a0cb..84fa5dc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,10 +10,38 @@ true + + + true + $(MSBuildThisFileDirectory)sqlbound.snk + 002400000480000094000000060200000024000052534131000400000100010099008ae592c0e403ce8ee0fad0005f95e61c709799847b62d56c375c739ac245fb993ccb7912a3d983024e1478ab13ba15094ae76aac09342324d3746124c97c9ac21cb8b877948400a7b7689f74ab55771620618f0a685fadb784a4a7636f9270647efb8a4fbaded853792609197b6fb2d271f178438f06a77c0746523a97ca + + + + + true + true + + Luís Amorim - 0.5.0 + 1.0.0-rc.1 Apache-2.0 + README.md + icon.png + sql;ado.net;dapper;source-generator;native-aot;sqlx;query;database;migrations https://github.com/lgamorim/sqlbound https://github.com/lgamorim/sqlbound git @@ -23,6 +51,14 @@ true + + + + + + + + + + + + + diff --git a/README.md b/README.md index 75c3ea0..7208782 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ SqlBound +

+ # SqlBound SqlBound is a .NET library providing [SQLx](https://github.com/launchbadge/sqlx)-equivalent @@ -6,8 +10,10 @@ reflection-free row materialization via a Roslyn incremental source generator, S migrations, and clean coexistence with [Dapper](https://github.com/DapperLib/Dapper) in the same project. -Status: **v0.5.0** — Phases 1–5 (Bedrock, Codegen, Verification, Providers, Migrations & CLI) are -complete. Phase 6 (Ship — API freeze and the 1.0 release) is still ahead; see [Roadmap](#roadmap). +Status: **v1.0.0-rc.1** — release candidate under review. Phases 1–5 (Bedrock, Codegen, Verification, +Providers, Migrations & CLI) are complete, and Phase 6 (Ship — API freeze and the 1.0 release) is in +progress; the stable `1.0.0` follows once the candidate is validated. See [Roadmap](#roadmap) and the +[changelog](CHANGELOG.md). ## Why @@ -19,12 +25,14 @@ turning a class of runtime SQL errors into compiler diagnostics. The two libraries are designed to coexist on the same `DbConnection`/`DbTransaction` with zero conflict — SqlBound never owns a connection and never defines `Query*`/`Execute*` extension -methods that would collide with Dapper's. +methods that would collide with Dapper's. See +[docs/dapper-coexistence.md](docs/dapper-coexistence.md) for a side-by-side guide. ## Quick start ```csharp using System.Data.Common; +using SqlBound; public static partial class ItemQueries { @@ -121,6 +129,7 @@ Architectural decisions are recorded as ADRs in [docs/adr/](docs/adr/): - [0005](docs/adr/0005-sqlite-describe-scope.md) — SQLite describe stays dry-run-only; computed columns and parameter types are out of scope - [0006](docs/adr/0006-migration-file-format.md) — migrations are paired up/down SQL files with a timestamp version and a checksummed ledger - [0007](docs/adr/0007-mysql-migrations-not-transactional.md) — MySQL migrations are not transactional, because MySQL commits DDL implicitly +- [0008](docs/adr/0008-api-stability-and-shipping.md) — the 1.0 public-API stability commitment, strong-naming, package signing, and the documentation site ## Performance @@ -144,8 +153,8 @@ minor version: | 3 — Verification | M7–M9 | Done (`v0.3.0`) | | 4 — Providers | M10–M12 | Done (`v0.4.0`) | | 5 — Migrations & CLI | M13–M15 | Done (`v0.5.0`) | -| 6 — Ship | M16 | Planned (`v1.0.0`) | +| 6 — Ship | M16 | Release candidate (`v1.0.0-rc.1`) | ## License -[Apache-2.0](LICENSE) +Copyright 2026 Luís Amorim. Licensed under the [Apache License 2.0](LICENSE). diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..5451903 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..1a84ecb Binary files /dev/null and b/assets/logo.png differ diff --git a/docfx.json b/docfx.json new file mode 100644 index 0000000..d088f00 --- /dev/null +++ b/docfx.json @@ -0,0 +1,48 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "src", + "files": [ + "SqlBound/SqlBound.csproj", + "SqlBound.Introspection/SqlBound.Introspection.csproj", + "SqlBound.Migrations/SqlBound.Migrations.csproj", + "SqlBound.SqlServer/SqlBound.SqlServer.csproj", + "SqlBound.Sqlite/SqlBound.Sqlite.csproj", + "SqlBound.Npgsql/SqlBound.Npgsql.csproj", + "SqlBound.MySql/SqlBound.MySql.csproj" + ] + } + ], + "dest": "api", + "properties": { + "TargetFramework": "net8.0" + } + } + ], + "build": { + "content": [ + { "files": [ "api/**.yml", "api/index.md" ] }, + { "files": [ "**.md", "**/toc.yml" ], "src": "docs", "dest": "docs", "exclude": [ "nuget/**" ] }, + { "files": [ "index.md", "toc.yml" ] } + ], + "resource": [ + { "files": [ "assets/logo.png" ] } + ], + "output": "_site", + "template": [ "default", "modern" ], + "globalMetadata": { + "_appName": "SqlBound", + "_appTitle": "SqlBound", + "_appLogoPath": "assets/logo.png", + "_description": "Compile-time verified SQL for .NET.", + "_enableSearch": true, + "_appFooter": "SqlBound — Apache-2.0", + "_gitContribute": { + "repo": "https://github.com/lgamorim/sqlbound", + "branch": "master" + } + } + } +} diff --git a/docs/adr/0008-api-stability-and-shipping.md b/docs/adr/0008-api-stability-and-shipping.md new file mode 100644 index 0000000..4c1c175 --- /dev/null +++ b/docs/adr/0008-api-stability-and-shipping.md @@ -0,0 +1,87 @@ +# 8. Shipping 1.0: API stability, strong-naming, signing, and docs + +## Status + +Accepted + +## Context + +Phase 6 (Ship, M16) turns SqlBound from a set of prerelease packages into a stable `1.0.0`. A `1.0` +release is a public commitment: from it onward, semantic versioning governs what may change, and +several decisions become effectively irreversible once consumers depend on the packages. This ADR +records the ones made for the release so they are not silently revisited later. + +The release is cut first as `1.0.0-rc.1` for review, then promoted to `1.0.0` unchanged; nothing in +this ADR is specific to the candidate versus the final. + +## Decision + +### Public-API stability, enforced mechanically + +After `1.0.0`, the public surface of every shipping library follows semantic versioning: additive in +minor releases, breaking only in a major. This is enforced by +`Microsoft.CodeAnalysis.PublicApiAnalyzers`: each library carries `PublicAPI.Shipped.txt` and +`PublicAPI.Unshipped.txt`, and any change to a public type or member that is not reflected in those +files fails the build (`RS0016`/`RS0017`) under `TreatWarningsAsErrors`. Reviewing a public-surface +change therefore means reviewing a baseline diff — the commitment is tool-checked, not prose. + +Until `1.0.0` actually ships to NuGet, the whole surface lives in `Unshipped.txt`; the GA promotion +moves it to `Shipped.txt`, after which removing a shipped entry is flagged as a breaking change. + +The surface was deliberately narrowed before the freeze (for example, the migration decision helpers +`MigrationPlan`, `MigrationReverter`, and `MigrationStatusReport` became `internal`). Widening a +surface after `1.0` is a non-breaking minor change; narrowing it is not — so anything whose public +exposure was incidental was pulled back while it was still free to do so. + +### Strong-naming + +All assemblies are strong-named with a single committed key (`sqlbound.snk`). A strong-named +assembly cannot reference an unsigned one, and SqlBound's entire peer group — EF Core, Dapper, +Npgsql, the `Microsoft.Data.*` providers — is strong-named; shipping unsigned would permanently +exclude strong-named consumers. Adding or removing a strong name changes assembly identity and is a +binary-breaking change, so `1.0` is the only free moment to decide. The key is committed to the repo +because strong naming is an *identity* mechanism, not a security boundary — the key is not a secret. + +### Package signing + +Packages rely on **nuget.org repository signing**, which is applied automatically on publish and +gives consumers integrity and provenance tied to the account. **Author signing** is not done for +`1.0`: it requires a code-signing certificate whose private key must (per current CA rules) live on +hardware or a cloud HSM, which is real cost and CI complexity for a marginal trust gain on an +open-source library. Author signing is purely additive and can be enabled in any later release +without a breaking change; the release workflow leaves room for it. + +### Documentation site + +A DocFX site is published to GitHub Pages. It generates the API reference from the same XML doc +comments the build already enforces (`GenerateDocumentationFile` + `CS1591`) and renders the +existing `docs/*.md` conceptual pages, so the site reuses committed content rather than duplicating +it. This was chosen over both a hand-maintained site and shipping `1.0` with no site at all. + +### Deferred publishing + +The release pipeline exists but does not publish on its own: a `v*.*.*` tag builds and packs, and +publishing to nuget.org is a separate, opt-in manual action. See the release workflow for the gate. + +## Consequences + +**Positive** + +- The `1.0` compatibility promise is enforced by the build, not by reviewer vigilance. +- Strong-named and unsigned consumers alike can reference SqlBound. +- Symbols, SourceLink, and deterministic CI builds let consumers step into exact committed sources. +- The documentation site stays in sync with the code because it is generated from it. + +**Negative / trade-offs** + +- Every intentional public-surface change now requires a baseline update in the same commit — + small, ongoing friction that is the point of the mechanism. +- The committed strong-name key means anyone can build an assembly with the same identity; this is + acceptable precisely because strong naming is not relied on as a security control. +- Without author signing, consumers who specifically require an author signature are not served at + `1.0`; this is revisited only if there is real demand. + +**Follow-up** + +- The GA promotion moves the API baselines from unshipped to shipped and drops the prerelease suffix. +- Author signing (Azure Trusted Signing + the `sign` tool) can be added later if warranted. diff --git a/docs/dapper-coexistence.md b/docs/dapper-coexistence.md new file mode 100644 index 0000000..9935096 --- /dev/null +++ b/docs/dapper-coexistence.md @@ -0,0 +1,107 @@ +# Using SqlBound alongside Dapper + +SqlBound and [Dapper](https://github.com/DapperLib/Dapper) are designed to run in the same project, +on the same `DbConnection` and `DbTransaction`, with no conflict. They solve different halves of the +data-access problem: + +- **SqlBound** handles SQL that is *static and known at build time*. It generates straight-line + ADO.NET reader code ahead of time and can verify the query against your schema before it runs. +- **Dapper** handles SQL that is *composed at runtime* — dynamic filters, search builders, `IN` + clauses of varying arity — mapping results with reflection. + +Reach for whichever fits the query in front of you. Nothing forces an either/or choice. + +## Why they don't collide + +Coexistence is a deliberate design constraint, not a happy accident. Concretely: + +1. **SqlBound never owns the connection.** Every entry point takes an already-open `DbConnection` + (and optionally a `DbTransaction`) that the caller controls. SqlBound never opens, closes, pools, + or wraps it — so Dapper can use the very same connection object. +2. **No method-name clash.** Dapper's surface is a set of `Query*` / `Execute*` / `ExecuteScalar*` + extension methods on `IDbConnection`. SqlBound deliberately defines **none** of those. Its + primary API is generated `static partial` methods you name yourself; its dynamic escape hatch, + `SqlSession`, uses distinct verbs (`RunAsync`, `FetchScalarAsync`). Importing both + `using SqlBound;` and `using Dapper;` in one file never produces an ambiguous-call error. +3. **No global configuration state.** SqlBound resolves type conversions at compile time in the + source generator; it has no equivalent of Dapper's static `SqlMapper` registry, so there is no + shared mutable state for the two libraries to fight over. + +## Sharing a connection + +Both libraries take the same open `DbConnection`. Use the generated method for the fixed query and +Dapper for the dynamic one: + +```csharp +using System.Data.Common; +using Dapper; +using SqlBound; + +public static partial class Catalog +{ + // Static, build-time-known query -> SqlBound generates the body and can verify it. + [SqlQuery("SELECT id AS Id, name AS Name, price AS Price FROM items WHERE category = @category ORDER BY id")] + public static partial Task> GetByCategoryAsync( + DbConnection connection, string category, CancellationToken cancellationToken = default); +} + +public sealed record Item(int Id, string Name, decimal? Price); + +// ... + +await connection.OpenAsync(); + +// SqlBound for the fixed query: +var byCategory = await Catalog.GetByCategoryAsync(connection, "books"); + +// Dapper on the same connection for a query composed at runtime: +var sql = $"SELECT * FROM items WHERE {BuildDynamicFilter(request)} ORDER BY id"; +var filtered = await connection.QueryAsync(sql, request.Parameters); +``` + +## Sharing a transaction + +Enlist both libraries in one `DbTransaction` — construct a `SqlSession` with it, and pass the same +transaction to Dapper's calls. Either library's writes participate in the same atomic unit: + +```csharp +await using var transaction = await connection.BeginTransactionAsync(); + +// SqlBound's dynamic surface, enlisted in the transaction: +var session = new SqlSession(connection, transaction); +await session.RunAsync( + "UPDATE accounts SET balance = balance - @amount WHERE id = @id", + new SqlParameters(("@amount", 100m), ("@id", fromId))); + +// Dapper, same transaction: +await connection.ExecuteAsync( + "UPDATE accounts SET balance = balance + @amount WHERE id = @id", + new { amount = 100m, id = toId }, + transaction); + +await transaction.CommitAsync(); +``` + +Generated `[SqlQuery]` / `[SqlExecute]` methods that accept a `DbTransaction` parameter enlist the +same way. + +## What each library is best at + +| Situation | Reach for | +| --- | --- | +| Query text fixed at build time | SqlBound `[SqlQuery]` / `[SqlExecute]` (verified, reflection-free) | +| Want compile-time schema checking | SqlBound (`sqlbound prepare` + analyzer) | +| Native AOT / trimming | SqlBound (no reflection, no IL emit) | +| SQL composed at runtime, mapped to types | Dapper `Query*` | +| One-off dynamic non-query or scalar, no generator | SqlBound `SqlSession.RunAsync` / `FetchScalarAsync`, or Dapper | + +SqlBound's `SqlSession` intentionally covers only non-query execution (`RunAsync`) and scalar reads +(`FetchScalarAsync`). Dynamic queries that materialize *rows* into types are Dapper's strength, +and SqlBound does not try to duplicate it. + +## Guaranteed by CI + +Coexistence is not just documented — it is a permanent regression test. The +`test/SqlBound.IntegrationTests` suite exercises SqlBound and Dapper against the same connection and +transaction on every CI run, so a future change that reintroduces a naming or lifecycle conflict +fails the build. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ce14c52 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,19 @@ +# Documentation + +Guides and design records for SqlBound. For the generated type-level reference, see the +[API Reference](xref:SqlBound). + +## Guides + +- [Compile-time verification](verification.md) — the `prepare` step, offline snapshots, and the + `SQLB###` diagnostics. +- [Diagnostics](diagnostics.md) — the full `SQLB###` catalog. +- [Introspection](introspection.md) — what each provider can and cannot describe. +- [Migrations](migrations.md) — the file format, the ledger, and per-provider behaviour. +- [Using SqlBound alongside Dapper](dapper-coexistence.md) — sharing a connection and transaction. +- [Benchmarks](benchmarks.md) — methodology and baseline numbers versus Dapper and raw ADO.NET. + +## Architecture decisions + +The [ADRs](adr/0001-verification-split.md) record the load-bearing design choices, from the +verification split through the 1.0 shipping decisions. diff --git a/docs/nuget/README.md b/docs/nuget/README.md new file mode 100644 index 0000000..acc223f --- /dev/null +++ b/docs/nuget/README.md @@ -0,0 +1,54 @@ +![SqlBound](https://raw.githubusercontent.com/lgamorim/sqlbound/master/assets/logo.png) + +# SqlBound + +**Compile-time verified SQL for .NET.** SqlBound checks your SQL against a real database schema at +build time and materializes rows with a Roslyn source generator — no runtime reflection, no IL emit, +Native AOT- and trimming-compatible. + +It is designed to **coexist with Dapper** on the same `DbConnection` and `DbTransaction`: SqlBound +targets static, build-time-known queries; Dapper stays the tool for runtime-composed SQL. SqlBound +never owns or wraps connections and defines no `Query*`/`Execute*` extension methods, so the two +libraries share a project without conflict. + +## Example + +```csharp +using System.Data.Common; +using SqlBound; + +public static partial class ItemQueries +{ + [SqlQuery("SELECT Id FROM dbo.Items")] + public static partial Task> GetIdsAsync(DbConnection connection); +} +``` + +The generator emits straight-line `DbDataReader` code. The `sqlbound prepare` CLI step verifies each +query against your schema and commits an offline JSON snapshot, so the in-IDE analyzer validates +without a live database connection. + +## Packages + +| Package | Purpose | +| --- | --- | +| `SqlBound` | Runtime core (dependency-free). | +| `SqlBound.Generators` | Source generator + analyzer (build-time only). | +| `SqlBound.Introspection` | Provider-neutral introspection contracts. | +| `SqlBound.Migrations` | SQL-file migration model and ledger. | +| `SqlBound.SqlServer` · `.Sqlite` · `.Npgsql` · `.MySql` | Per-provider introspection and type mapping. | +| `sqlbound` (dotnet tool) | CLI: `prepare`, `verify`, `migrate`, `database`. | + +## Providers + +SQL Server, SQLite, PostgreSQL, and MySQL — query verification, reflection-free materialization, and +SQL-file migrations, each reflecting what the underlying engine actually supports. + +## Documentation + +- Guides, provider matrix, and API reference: https://lgamorim.github.io/sqlbound/ +- Source, issues, and changelog: https://github.com/lgamorim/sqlbound + +## License + +Copyright 2026 Luís Amorim. Licensed under the [Apache License 2.0](https://github.com/lgamorim/sqlbound/blob/master/LICENSE). diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..7d465c6 --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,34 @@ +- name: Overview + href: index.md +- name: Guides + items: + - name: Compile-time verification + href: verification.md + - name: Diagnostics (SQLB###) + href: diagnostics.md + - name: Introspection + href: introspection.md + - name: Migrations + href: migrations.md + - name: Using SqlBound alongside Dapper + href: dapper-coexistence.md + - name: Benchmarks + href: benchmarks.md +- name: Architecture Decisions + items: + - name: 0001 — Verification split + href: adr/0001-verification-split.md + - name: 0002 — Generator snapshot consumption + href: adr/0002-generator-snapshot-consumption.md + - name: 0003 — Verification opt-in by snapshot presence + href: adr/0003-verification-opt-in-by-snapshot-presence.md + - name: 0004 — prepare is CLI-only + href: adr/0004-prepare-is-cli-only.md + - name: 0005 — SQLite describe scope + href: adr/0005-sqlite-describe-scope.md + - name: 0006 — Migration file format + href: adr/0006-migration-file-format.md + - name: 0007 — MySQL migrations not transactional + href: adr/0007-mysql-migrations-not-transactional.md + - name: 0008 — API stability and shipping + href: adr/0008-api-stability-and-shipping.md diff --git a/index.md b/index.md new file mode 100644 index 0000000..f18655c --- /dev/null +++ b/index.md @@ -0,0 +1,25 @@ +# SqlBound + +**Compile-time verified SQL for .NET.** SqlBound checks your SQL against a real database schema at +build time and materializes rows with a Roslyn source generator — no runtime reflection, no IL emit, +Native AOT- and trimming-compatible. It coexists cleanly with [Dapper](https://github.com/DapperLib/Dapper) +on the same connection and transaction. + +## Get started + +- **[Documentation](docs/index.md)** — verification, migrations, providers, and the Dapper coexistence guide. +- **[API Reference](xref:SqlBound)** — the full public surface of every package. +- **[Source on GitHub](https://github.com/lgamorim/sqlbound)** — issues, releases, and the changelog. + +```csharp +using SqlBound; + +public static partial class ItemQueries +{ + [SqlQuery("SELECT Id FROM dbo.Items")] + public static partial Task> GetIdsAsync(DbConnection connection); +} +``` + +The generator emits the method body. `sqlbound prepare` verifies the query against your schema and +commits an offline snapshot, so the in-IDE analyzer validates without a live database connection. diff --git a/sqlbound.snk b/sqlbound.snk new file mode 100644 index 0000000..06c750f Binary files /dev/null and b/sqlbound.snk differ diff --git a/src/SqlBound.Cli/SqlBound.Cli.csproj b/src/SqlBound.Cli/SqlBound.Cli.csproj index 6c9045c..6329977 100644 --- a/src/SqlBound.Cli/SqlBound.Cli.csproj +++ b/src/SqlBound.Cli/SqlBound.Cli.csproj @@ -12,6 +12,7 @@ true sqlbound SqlBound command-line tool: prepare offline query snapshots, scaffold migrations, and manage the database. + $(PackageTags);dotnet-tool;cli diff --git a/src/SqlBound.Introspection/PublicAPI.Shipped.txt b/src/SqlBound.Introspection/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound.Introspection/PublicAPI.Unshipped.txt b/src/SqlBound.Introspection/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..78d4e3e --- /dev/null +++ b/src/SqlBound.Introspection/PublicAPI.Unshipped.txt @@ -0,0 +1,33 @@ +#nullable enable +SqlBound.Introspection.DescribedColumn +SqlBound.Introspection.DescribedColumn.ClrTypeText.get -> string! +SqlBound.Introspection.DescribedColumn.ClrTypeText.init -> void +SqlBound.Introspection.DescribedColumn.DescribedColumn(int Ordinal, string! Name, string! SqlTypeName, string! ClrTypeText, bool IsNullable) -> void +SqlBound.Introspection.DescribedColumn.IsNullable.get -> bool +SqlBound.Introspection.DescribedColumn.IsNullable.init -> void +SqlBound.Introspection.DescribedColumn.Name.get -> string! +SqlBound.Introspection.DescribedColumn.Name.init -> void +SqlBound.Introspection.DescribedColumn.Ordinal.get -> int +SqlBound.Introspection.DescribedColumn.Ordinal.init -> void +SqlBound.Introspection.DescribedColumn.SqlTypeName.get -> string! +SqlBound.Introspection.DescribedColumn.SqlTypeName.init -> void +SqlBound.Introspection.DescribedParameter +SqlBound.Introspection.DescribedParameter.ClrTypeText.get -> string? +SqlBound.Introspection.DescribedParameter.ClrTypeText.init -> void +SqlBound.Introspection.DescribedParameter.DescribedParameter(string! Name, string! SqlTypeName, string? ClrTypeText) -> void +SqlBound.Introspection.DescribedParameter.Name.get -> string! +SqlBound.Introspection.DescribedParameter.Name.init -> void +SqlBound.Introspection.DescribedParameter.SqlTypeName.get -> string! +SqlBound.Introspection.DescribedParameter.SqlTypeName.init -> void +SqlBound.Introspection.IQueryDescriber +SqlBound.Introspection.IQueryDescriber.DescribeAsync(System.Data.Common.DbConnection! connection, string! commandText, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.Introspection.QueryDescription +SqlBound.Introspection.QueryDescription.Columns.get -> System.Collections.Generic.IReadOnlyList! +SqlBound.Introspection.QueryDescription.Columns.init -> void +SqlBound.Introspection.QueryDescription.Parameters.get -> System.Collections.Generic.IReadOnlyList! +SqlBound.Introspection.QueryDescription.Parameters.init -> void +SqlBound.Introspection.QueryDescription.QueryDescription(System.Collections.Generic.IReadOnlyList! Columns, System.Collections.Generic.IReadOnlyList! Parameters) -> void +SqlBound.Introspection.SqlBoundDescribeException +SqlBound.Introspection.SqlBoundDescribeException.CommandText.get -> string! +SqlBound.Introspection.SqlBoundDescribeException.SqlBoundDescribeException(string! message, string! commandText) -> void +SqlBound.Introspection.SqlBoundDescribeException.SqlBoundDescribeException(string! message, string! commandText, System.Exception! innerException) -> void diff --git a/src/SqlBound.Introspection/SqlBound.Introspection.csproj b/src/SqlBound.Introspection/SqlBound.Introspection.csproj index bb9e3f2..6e98257 100644 --- a/src/SqlBound.Introspection/SqlBound.Introspection.csproj +++ b/src/SqlBound.Introspection/SqlBound.Introspection.csproj @@ -3,6 +3,7 @@ Provider-neutral database introspection contracts for SqlBound's prepare step. true + true diff --git a/src/SqlBound.Migrations/MigrationPlan.cs b/src/SqlBound.Migrations/MigrationPlan.cs index e6699c7..7b93ec7 100644 --- a/src/SqlBound.Migrations/MigrationPlan.cs +++ b/src/SqlBound.Migrations/MigrationPlan.cs @@ -7,7 +7,7 @@ namespace SqlBound.Migrations; /// that is already applied. /// /// The migrations to apply, ascending by version. -public sealed record MigrationPlan(IReadOnlyList Pending) +internal sealed record MigrationPlan(IReadOnlyList Pending) { /// Computes the plan, validating the directory against the applied history. /// The migrations on disk, as loaded from the directory. @@ -15,7 +15,7 @@ public sealed record MigrationPlan(IReadOnlyList Pending) /// /// An applied migration's checksum has drifted, or a pending migration is out of order. /// - public static MigrationPlan Create( + internal static MigrationPlan Create( IReadOnlyList migrations, IReadOnlyList applied) { var appliedByVersion = applied.ToDictionary(row => row.Version); diff --git a/src/SqlBound.Migrations/MigrationReverter.cs b/src/SqlBound.Migrations/MigrationReverter.cs index 4050e23..9a614d2 100644 --- a/src/SqlBound.Migrations/MigrationReverter.cs +++ b/src/SqlBound.Migrations/MigrationReverter.cs @@ -5,7 +5,7 @@ namespace SqlBound.Migrations; /// selection fails if that migration's files are gone or it ships no down script, so the caller /// never runs a rollback it cannot honor. /// -public static class MigrationReverter +internal static class MigrationReverter { /// Chooses the migration to revert. /// The migrations on disk, as loaded from the directory. @@ -14,7 +14,7 @@ public static class MigrationReverter /// /// The most recently applied migration is missing from disk or is irreversible. /// - public static Migration? Plan(IReadOnlyList migrations, IReadOnlyList applied) + internal static Migration? Plan(IReadOnlyList migrations, IReadOnlyList applied) { if (applied.Count == 0) { diff --git a/src/SqlBound.Migrations/MigrationStatusReport.cs b/src/SqlBound.Migrations/MigrationStatusReport.cs index 8837607..74efe00 100644 --- a/src/SqlBound.Migrations/MigrationStatusReport.cs +++ b/src/SqlBound.Migrations/MigrationStatusReport.cs @@ -5,13 +5,13 @@ namespace SqlBound.Migrations; /// ordered by version. This is the read-only view behind migrate status; unlike /// it reports drift and missing migrations rather than throwing on them. /// -public static class MigrationStatusReport +internal static class MigrationStatusReport { /// Classifies every known migration into its state relative to the ledger. /// The migrations on disk, as loaded from the directory. /// The migrations the ledger records as applied. /// One status per migration known to either source, ascending by version. - public static IReadOnlyList Build( + internal static IReadOnlyList Build( IReadOnlyList migrations, IReadOnlyList applied) { var migrationsByVersion = migrations.ToDictionary(migration => migration.Version); diff --git a/src/SqlBound.Migrations/PublicAPI.Shipped.txt b/src/SqlBound.Migrations/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound.Migrations/PublicAPI.Unshipped.txt b/src/SqlBound.Migrations/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..eca605e --- /dev/null +++ b/src/SqlBound.Migrations/PublicAPI.Unshipped.txt @@ -0,0 +1,64 @@ +#nullable enable +SqlBound.Migrations.AppliedMigration +SqlBound.Migrations.AppliedMigration.AppliedMigration(long Version, string! Name, string! Checksum, System.DateTime AppliedOnUtc, long ExecutionMs) -> void +SqlBound.Migrations.AppliedMigration.AppliedOnUtc.get -> System.DateTime +SqlBound.Migrations.AppliedMigration.AppliedOnUtc.init -> void +SqlBound.Migrations.AppliedMigration.Checksum.get -> string! +SqlBound.Migrations.AppliedMigration.Checksum.init -> void +SqlBound.Migrations.AppliedMigration.ExecutionMs.get -> long +SqlBound.Migrations.AppliedMigration.ExecutionMs.init -> void +SqlBound.Migrations.AppliedMigration.Name.get -> string! +SqlBound.Migrations.AppliedMigration.Name.init -> void +SqlBound.Migrations.AppliedMigration.Version.get -> long +SqlBound.Migrations.AppliedMigration.Version.init -> void +SqlBound.Migrations.IDatabaseAdmin +SqlBound.Migrations.IDatabaseAdmin.CreateAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Migrations.IDatabaseAdmin.DropAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Migrations.IMigrationLedger +SqlBound.Migrations.IMigrationLedger.EnsureCreatedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Migrations.IMigrationLedger.GetAppliedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +SqlBound.Migrations.IMigrationLedger.RecordAppliedAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, SqlBound.Migrations.AppliedMigration! migration, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Migrations.IMigrationLedger.RemoveAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, long version, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Migrations.IMigrationLedger.SupportsTransactionalDdl.get -> bool +SqlBound.Migrations.Migration +SqlBound.Migrations.Migration.Checksum.get -> string! +SqlBound.Migrations.Migration.Checksum.init -> void +SqlBound.Migrations.Migration.DownScript.get -> string? +SqlBound.Migrations.Migration.DownScript.init -> void +SqlBound.Migrations.Migration.IsReversible.get -> bool +SqlBound.Migrations.Migration.Migration(long Version, string! Name, string! UpScript, string? DownScript, string! Checksum) -> void +SqlBound.Migrations.Migration.Name.get -> string! +SqlBound.Migrations.Migration.Name.init -> void +SqlBound.Migrations.Migration.UpScript.get -> string! +SqlBound.Migrations.Migration.UpScript.init -> void +SqlBound.Migrations.Migration.Version.get -> long +SqlBound.Migrations.Migration.Version.init -> void +SqlBound.Migrations.MigrationDirectory +SqlBound.Migrations.MigrationExecutionException +SqlBound.Migrations.MigrationExecutionException.MigrationExecutionException(long version, string! name, System.Exception! innerException) -> void +SqlBound.Migrations.MigrationExecutionException.Name.get -> string! +SqlBound.Migrations.MigrationExecutionException.Version.get -> long +SqlBound.Migrations.MigrationFormatException +SqlBound.Migrations.MigrationFormatException.MigrationFormatException(string! message) -> void +SqlBound.Migrations.MigrationInconsistencyException +SqlBound.Migrations.MigrationInconsistencyException.MigrationInconsistencyException(string! message) -> void +SqlBound.Migrations.MigrationRunner +SqlBound.Migrations.MigrationState +SqlBound.Migrations.MigrationState.Applied = 0 -> SqlBound.Migrations.MigrationState +SqlBound.Migrations.MigrationState.Drifted = 2 -> SqlBound.Migrations.MigrationState +SqlBound.Migrations.MigrationState.Missing = 3 -> SqlBound.Migrations.MigrationState +SqlBound.Migrations.MigrationState.Pending = 1 -> SqlBound.Migrations.MigrationState +SqlBound.Migrations.MigrationStatus +SqlBound.Migrations.MigrationStatus.AppliedOnUtc.get -> System.DateTime? +SqlBound.Migrations.MigrationStatus.AppliedOnUtc.init -> void +SqlBound.Migrations.MigrationStatus.MigrationStatus(long Version, string! Name, SqlBound.Migrations.MigrationState State, System.DateTime? AppliedOnUtc) -> void +SqlBound.Migrations.MigrationStatus.Name.get -> string! +SqlBound.Migrations.MigrationStatus.Name.init -> void +SqlBound.Migrations.MigrationStatus.State.get -> SqlBound.Migrations.MigrationState +SqlBound.Migrations.MigrationStatus.State.init -> void +SqlBound.Migrations.MigrationStatus.Version.get -> long +SqlBound.Migrations.MigrationStatus.Version.init -> void +static SqlBound.Migrations.MigrationDirectory.Load(string! path) -> System.Collections.Generic.IReadOnlyList! +static SqlBound.Migrations.MigrationRunner.RevertAsync(System.Data.Common.DbConnection! connection, SqlBound.Migrations.IMigrationLedger! ledger, System.Collections.Generic.IReadOnlyList! migrations, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static SqlBound.Migrations.MigrationRunner.RunAsync(System.Data.Common.DbConnection! connection, SqlBound.Migrations.IMigrationLedger! ledger, System.Collections.Generic.IReadOnlyList! migrations, System.TimeProvider! timeProvider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +static SqlBound.Migrations.MigrationRunner.StatusAsync(System.Data.Common.DbConnection! connection, SqlBound.Migrations.IMigrationLedger! ledger, System.Collections.Generic.IReadOnlyList! migrations, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! diff --git a/src/SqlBound.Migrations/SqlBound.Migrations.csproj b/src/SqlBound.Migrations/SqlBound.Migrations.csproj index 75e6b37..dfe3922 100644 --- a/src/SqlBound.Migrations/SqlBound.Migrations.csproj +++ b/src/SqlBound.Migrations/SqlBound.Migrations.csproj @@ -3,6 +3,7 @@ Provider-neutral SQL-file migration model and ledger contracts for SqlBound. true + true diff --git a/src/SqlBound.MySql/MySqlParameterScanner.cs b/src/SqlBound.MySql/MySqlParameterScanner.cs index f0218ae..77edd9a 100644 --- a/src/SqlBound.MySql/MySqlParameterScanner.cs +++ b/src/SqlBound.MySql/MySqlParameterScanner.cs @@ -6,8 +6,9 @@ namespace SqlBound.MySql; /// names — MySqlQueryDescriber must pre-declare them before it can even describe a /// statement's columns — so this is a hand-rolled scan rather than a server round-trip. It skips /// single- and double-quoted string literals (both doubled-quote and backslash escaping), -/// backtick-quoted identifiers, and --/#//* */ comments, so a literal like -/// 'user@example.com' is never mistaken for a placeholder. +/// backtick-quoted identifiers, --/#//* */ comments, and @@ system +/// variables, so a literal like 'user@example.com' or a variable like @@sql_mode is +/// never mistaken for a placeholder. /// internal static class MySqlParameterScanner { @@ -24,13 +25,21 @@ public static IReadOnlyList ExtractNames(string commandText) case '\'' or '"' or '`': position = SkipQuoted(commandText, position, current); break; - case '-' when Peek(commandText, position + 1) == '-': + // MySQL only recognizes -- as a comment when followed by whitespace or the end + // of the statement; "1--@x" is the arithmetic expression 1 - (-@x). + case '-' when Peek(commandText, position + 1) == '-' + && (position + 2 >= commandText.Length || char.IsWhiteSpace(commandText[position + 2])): case '#': position = SkipLineComment(commandText, position); break; case '/' when Peek(commandText, position + 1) == '*': position = SkipBlockComment(commandText, position); break; + // @@name is a system variable (optionally scoped, e.g. @@session.sql_mode), + // never a parameter placeholder. + case '@' when Peek(commandText, position + 1) == '@': + position = SkipSystemVariable(commandText, position); + break; case '@' when IsIdentifierStart(Peek(commandText, position + 1)): position = ReadParameterName(commandText, position, names, seen); break; @@ -90,6 +99,17 @@ private static int SkipQuoted(string text, int position, char quote) return position; } + private static int SkipSystemVariable(string text, int position) + { + position += 2; + while (position < text.Length && (IsIdentifierPart(text[position]) || text[position] == '.')) + { + position++; + } + + return position; + } + private static int SkipLineComment(string text, int position) { var newline = text.IndexOf('\n', position); diff --git a/src/SqlBound.MySql/PublicAPI.Shipped.txt b/src/SqlBound.MySql/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound.MySql/PublicAPI.Unshipped.txt b/src/SqlBound.MySql/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..07cd194 --- /dev/null +++ b/src/SqlBound.MySql/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +#nullable enable +SqlBound.MySql.MySqlDatabaseAdmin +SqlBound.MySql.MySqlDatabaseAdmin.CreateAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.MySql.MySqlDatabaseAdmin.DropAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.MySql.MySqlDatabaseAdmin.MySqlDatabaseAdmin() -> void +SqlBound.MySql.MySqlMigrationLedger +SqlBound.MySql.MySqlMigrationLedger.EnsureCreatedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.MySql.MySqlMigrationLedger.GetAppliedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +SqlBound.MySql.MySqlMigrationLedger.MySqlMigrationLedger() -> void +SqlBound.MySql.MySqlMigrationLedger.RecordAppliedAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, SqlBound.Migrations.AppliedMigration! migration, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.MySql.MySqlMigrationLedger.RemoveAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, long version, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.MySql.MySqlMigrationLedger.SupportsTransactionalDdl.get -> bool +SqlBound.MySql.MySqlQueryDescriber +SqlBound.MySql.MySqlQueryDescriber.DescribeAsync(System.Data.Common.DbConnection! connection, string! commandText, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.MySql.MySqlQueryDescriber.MySqlQueryDescriber() -> void diff --git a/src/SqlBound.MySql/SqlBound.MySql.csproj b/src/SqlBound.MySql/SqlBound.MySql.csproj index c613873..5e00d5a 100644 --- a/src/SqlBound.MySql/SqlBound.MySql.csproj +++ b/src/SqlBound.MySql/SqlBound.MySql.csproj @@ -2,6 +2,8 @@ MySQL introspection and type mapping for SqlBound. + true + $(PackageTags);mysql diff --git a/src/SqlBound.Npgsql/PublicAPI.Shipped.txt b/src/SqlBound.Npgsql/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound.Npgsql/PublicAPI.Unshipped.txt b/src/SqlBound.Npgsql/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..f5868b2 --- /dev/null +++ b/src/SqlBound.Npgsql/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +#nullable enable +SqlBound.Npgsql.NpgsqlDatabaseAdmin +SqlBound.Npgsql.NpgsqlDatabaseAdmin.CreateAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Npgsql.NpgsqlDatabaseAdmin.DropAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Npgsql.NpgsqlDatabaseAdmin.NpgsqlDatabaseAdmin() -> void +SqlBound.Npgsql.NpgsqlMigrationLedger +SqlBound.Npgsql.NpgsqlMigrationLedger.EnsureCreatedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Npgsql.NpgsqlMigrationLedger.GetAppliedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +SqlBound.Npgsql.NpgsqlMigrationLedger.NpgsqlMigrationLedger() -> void +SqlBound.Npgsql.NpgsqlMigrationLedger.RecordAppliedAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, SqlBound.Migrations.AppliedMigration! migration, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Npgsql.NpgsqlMigrationLedger.RemoveAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, long version, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Npgsql.NpgsqlMigrationLedger.SupportsTransactionalDdl.get -> bool +SqlBound.Npgsql.NpgsqlQueryDescriber +SqlBound.Npgsql.NpgsqlQueryDescriber.DescribeAsync(System.Data.Common.DbConnection! connection, string! commandText, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.Npgsql.NpgsqlQueryDescriber.NpgsqlQueryDescriber() -> void diff --git a/src/SqlBound.Npgsql/SqlBound.Npgsql.csproj b/src/SqlBound.Npgsql/SqlBound.Npgsql.csproj index 05c3161..24b4fa7 100644 --- a/src/SqlBound.Npgsql/SqlBound.Npgsql.csproj +++ b/src/SqlBound.Npgsql/SqlBound.Npgsql.csproj @@ -2,6 +2,8 @@ PostgreSQL introspection and type mapping for SqlBound. + true + $(PackageTags);postgresql;postgres;npgsql diff --git a/src/SqlBound.SqlServer/PublicAPI.Shipped.txt b/src/SqlBound.SqlServer/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound.SqlServer/PublicAPI.Unshipped.txt b/src/SqlBound.SqlServer/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..17e37fd --- /dev/null +++ b/src/SqlBound.SqlServer/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +#nullable enable +SqlBound.SqlServer.SqlServerDatabaseAdmin +SqlBound.SqlServer.SqlServerDatabaseAdmin.CreateAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.SqlServer.SqlServerDatabaseAdmin.DropAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.SqlServer.SqlServerDatabaseAdmin.SqlServerDatabaseAdmin() -> void +SqlBound.SqlServer.SqlServerMigrationLedger +SqlBound.SqlServer.SqlServerMigrationLedger.EnsureCreatedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.SqlServer.SqlServerMigrationLedger.GetAppliedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +SqlBound.SqlServer.SqlServerMigrationLedger.RecordAppliedAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, SqlBound.Migrations.AppliedMigration! migration, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.SqlServer.SqlServerMigrationLedger.RemoveAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, long version, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.SqlServer.SqlServerMigrationLedger.SqlServerMigrationLedger() -> void +SqlBound.SqlServer.SqlServerMigrationLedger.SupportsTransactionalDdl.get -> bool +SqlBound.SqlServer.SqlServerQueryDescriber +SqlBound.SqlServer.SqlServerQueryDescriber.DescribeAsync(System.Data.Common.DbConnection! connection, string! commandText, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.SqlServer.SqlServerQueryDescriber.SqlServerQueryDescriber() -> void diff --git a/src/SqlBound.SqlServer/SqlBound.SqlServer.csproj b/src/SqlBound.SqlServer/SqlBound.SqlServer.csproj index b91a46c..f8f50b8 100644 --- a/src/SqlBound.SqlServer/SqlBound.SqlServer.csproj +++ b/src/SqlBound.SqlServer/SqlBound.SqlServer.csproj @@ -2,6 +2,8 @@ SQL Server introspection and type mapping for SqlBound. + true + $(PackageTags);sqlserver;mssql diff --git a/src/SqlBound.Sqlite/PublicAPI.Shipped.txt b/src/SqlBound.Sqlite/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound.Sqlite/PublicAPI.Unshipped.txt b/src/SqlBound.Sqlite/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..d70e3f5 --- /dev/null +++ b/src/SqlBound.Sqlite/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +#nullable enable +SqlBound.Sqlite.SqliteDatabaseAdmin +SqlBound.Sqlite.SqliteDatabaseAdmin.CreateAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Sqlite.SqliteDatabaseAdmin.DropAsync(string! connectionString, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Sqlite.SqliteDatabaseAdmin.SqliteDatabaseAdmin() -> void +SqlBound.Sqlite.SqliteMigrationLedger +SqlBound.Sqlite.SqliteMigrationLedger.EnsureCreatedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Sqlite.SqliteMigrationLedger.GetAppliedAsync(System.Data.Common.DbConnection! connection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +SqlBound.Sqlite.SqliteMigrationLedger.RecordAppliedAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, SqlBound.Migrations.AppliedMigration! migration, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Sqlite.SqliteMigrationLedger.RemoveAsync(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction, long version, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +SqlBound.Sqlite.SqliteMigrationLedger.SqliteMigrationLedger() -> void +SqlBound.Sqlite.SqliteMigrationLedger.SupportsTransactionalDdl.get -> bool +SqlBound.Sqlite.SqliteQueryDescriber +SqlBound.Sqlite.SqliteQueryDescriber.DescribeAsync(System.Data.Common.DbConnection! connection, string! commandText, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.Sqlite.SqliteQueryDescriber.SqliteQueryDescriber() -> void diff --git a/src/SqlBound.Sqlite/SqlBound.Sqlite.csproj b/src/SqlBound.Sqlite/SqlBound.Sqlite.csproj index 1a6c51b..1048425 100644 --- a/src/SqlBound.Sqlite/SqlBound.Sqlite.csproj +++ b/src/SqlBound.Sqlite/SqlBound.Sqlite.csproj @@ -2,6 +2,8 @@ SQLite introspection and type mapping for SqlBound. + true + $(PackageTags);sqlite diff --git a/src/SqlBound.Sqlite/SqliteDatabaseAdmin.cs b/src/SqlBound.Sqlite/SqliteDatabaseAdmin.cs index d07250e..636d3ba 100644 --- a/src/SqlBound.Sqlite/SqliteDatabaseAdmin.cs +++ b/src/SqlBound.Sqlite/SqliteDatabaseAdmin.cs @@ -6,7 +6,8 @@ namespace SqlBound.Sqlite; /// /// The SQLite implementation of . SQLite has no server: the "database" /// is the file named by the connection string's Data Source. Creating it opens a connection -/// (which materializes the file); dropping it deletes the file. Both are idempotent. +/// (which materializes the file); dropping it deletes the file and any -wal/-shm +/// sidecars a crashed process may have left beside it. Both are idempotent. /// public sealed class SqliteDatabaseAdmin : IDatabaseAdmin { @@ -24,11 +25,16 @@ public Task DropAsync(string connectionString, CancellationToken cancell { var dataSource = DataSourceOf(connectionString); - // Release any pooled handles so the file is not locked when we delete it. + // Release any pooled handles so the files are not locked when we delete them. The WAL + // sidecars are normally checkpointed away on a clean close, but a crashed process leaves + // them behind — and stale ones would poison a database later created at the same path. SqliteConnection.ClearAllPools(); - if (File.Exists(dataSource)) + foreach (var path in new[] { dataSource, $"{dataSource}-wal", $"{dataSource}-shm" }) { - File.Delete(dataSource); + if (File.Exists(path)) + { + File.Delete(path); + } } return Task.FromResult(dataSource); diff --git a/src/SqlBound/PublicAPI.Shipped.txt b/src/SqlBound/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/SqlBound/PublicAPI.Unshipped.txt b/src/SqlBound/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..03c6d1e --- /dev/null +++ b/src/SqlBound/PublicAPI.Unshipped.txt @@ -0,0 +1,16 @@ +#nullable enable +SqlBound.SqlExecuteAttribute +SqlBound.SqlExecuteAttribute.CommandText.get -> string! +SqlBound.SqlExecuteAttribute.SqlExecuteAttribute(string! commandText) -> void +SqlBound.SqlParameters +SqlBound.SqlParameters.Count.get -> int +SqlBound.SqlParameters.Items.get -> System.Collections.Generic.IReadOnlyList>! +SqlBound.SqlParameters.SqlParameters(params (string! Name, object? Value)[]! parameters) -> void +SqlBound.SqlQueryAttribute +SqlBound.SqlQueryAttribute.CommandText.get -> string! +SqlBound.SqlQueryAttribute.SqlQueryAttribute(string! commandText) -> void +SqlBound.SqlSession +SqlBound.SqlSession.FetchScalarAsync(string! sql, SqlBound.SqlParameters? parameters = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.SqlSession.RunAsync(string! sql, SqlBound.SqlParameters? parameters = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +SqlBound.SqlSession.SqlSession(System.Data.Common.DbConnection! connection, System.Data.Common.DbTransaction? transaction = null) -> void +static SqlBound.SqlParameters.Empty.get -> SqlBound.SqlParameters! diff --git a/src/SqlBound/SqlBound.csproj b/src/SqlBound/SqlBound.csproj index 0d6574b..3e15a35 100644 --- a/src/SqlBound/SqlBound.csproj +++ b/src/SqlBound/SqlBound.csproj @@ -2,6 +2,7 @@ Compile-time verified SQL for .NET. + true