Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .claude/rules/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Architecture

Rules governing physical project and solution structure (as opposed to logical design, which lives in `design-principles.md`):

- Repo/solution layout: source under `src/<ProjectName>/`, tests under `test/<ProjectName>.UnitTests/` (singular `test`), 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.
13 changes: 13 additions & 0 deletions .claude/rules/coding-standards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Coding Standards

Always follow the Microsoft C# coding conventions and .NET naming guidelines:

- PascalCase for types, methods, properties, constants, and public members; camelCase for locals and parameters; `_camelCase` for private fields (`private static readonly` values are constant-like and stay PascalCase); `I` prefix for interfaces; `Async` suffix for async methods.
- One top-level type per file; file name matches the type name. Exception: a small supporting type (typically a record or struct) that exists only to serve the file's primary type — a companion DTO, result, or value type it owns and produces — may be declared in the same file. The exception covers helpers bound to that one type; it does not license unrelated types or a type intended for reuse across the codebase, which still get their own file.
- Use file-scoped namespaces, `var` when the type is apparent, expression-bodied members only when they improve readability.
- Enable and respect nullable reference types (`<Nullable>enable</Nullable>`); never suppress warnings with `!` without a comment justifying it.
- Prefer records for immutable data, `readonly` where possible, and pattern matching over type checks/casts. Exception: ORM-mapped entities that require mutable, parameterless-constructible state (e.g., EF Core) may remain classes.
- Public APIs must have XML doc comments; internal code is documented only where intent is not obvious from the code. Test projects are exempt — their `public` types exist only for test-framework discovery, not as a consumed API surface.
- Never include unnecessary using directives.
- Codify these conventions in an `.editorconfig` at the solution root (naming rules, `file_scoped` namespaces, `IDE0005` for unused usings, etc.) so they are tool-enforced rather than prose-only, and set `<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>` so style violations fail the build alongside `TreatWarningsAsErrors`.
- All code must pass `dotnet format` and build with zero warnings (`TreatWarningsAsErrors` is on).
11 changes: 11 additions & 0 deletions .claude/rules/design-principles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Design Principles

- Always apply well-established object-oriented design practices: favor composition over inheritance, encapsulate invariants inside domain types, keep classes small and cohesive, and depend on abstractions at layer boundaries.
- Adhere to SOLID to keep the code adaptive to change:
- **S** — one reason to change per class; split classes that mix concerns (e.g., validation + persistence).
- **O** — extend behavior via new implementations of existing abstractions rather than modifying stable code with switch/if chains on type.
- **L** — subtypes must honor base contracts; never throw `NotSupportedException` from an inherited member.
- **I** — prefer small, role-specific interfaces (e.g., `IOrderReader` / `IOrderWriter`) over fat ones.
- **D** — high-level modules (Application, Domain) define interfaces; Infrastructure implements them. Dependencies always point inward.
- Apply design patterns only when there is a tangible benefit compared to simpler alternatives. State the benefit in the PR/commit description when introducing one (e.g., Strategy to eliminate a growing conditional; Decorator for cross-cutting behavior). Do not introduce a pattern speculatively.
- IMPORTANT: Do not overengineer. No abstractions "for the future," no interfaces with a single implementation unless required for testing or layer isolation, no generic frameworks for one use case. Prefer the simplest design that satisfies current requirements and remains easy to change (YAGNI).
19 changes: 19 additions & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Testing — Test-Driven Development

YOU MUST follow a test-driven development approach for all production code:

1. **Red** — write a failing test that specifies the desired behavior before writing implementation code.
2. **Green** — write the minimum implementation to make the test pass.
3. **Refactor** — clean up the code and tests while keeping everything green.

Rules:

- Never write production code without a failing test that motivates it. Never mark a task complete while any test fails.
- Ensure all relevant edge cases are covered: null/empty inputs, boundary values, invalid state transitions, concurrency where applicable, and failure paths (exceptions, timeouts, cancellation via `CancellationToken`).
- When a task explicitly scopes test coverage (e.g., "one test", "happy path only"), that explicit scope takes precedence over this checklist — but say so out loud (commit message, notes, or conversation) rather than silently under-testing.
- Test naming: `Should_ExpectedOutcome_When_Scenario` (e.g., `Should_ThrowInsufficientStockException_When_InventoryInsufficient`).
- Structure tests as Arrange–Act–Assert.
- Unit tests must be fast and deterministic: no real I/O, network, clock, or `Task.Delay`. Abstract time behind `TimeProvider`.
- Run `dotnet test` after every change and before declaring any work finished.
- Generator output is snapshot-tested with the Roslyn testing SDK.
- Benchmarks (BenchmarkDotNet, vs Dapper and raw ADO.NET) live in the repo.
13 changes: 13 additions & 0 deletions .claude/rules/workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Workflow

- For each milestone, draft a plan first and present it to the user; execution starts only after the user approves the plan.
- Never open a pull request automatically — always confirm with the user first.
- Make minimal, focused changes; do not refactor unrelated code in the same change.
- One logical change per commit, with an imperative-mood message explaining *why*.
- When unsure between two designs, present both with trade-offs and ask before implementing.
- Never commit or push directly to the default branch (`master`/`main`). Milestones are used to track the progress of the project. All work happens on a `feature/M<number>-<desc>`-prefixed branch.
- The branch lands on the default branch only through a reviewed pull request. Enforce this with branch protection (require a PR, disallow direct pushes, require the squash-merge strategy) so it can't be bypassed by accident.
- 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.
- Update this file when a new convention or correction is established.
97 changes: 97 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

[*.cs]
indent_size = 4

# Language style
csharp_style_namespace_declarations = file_scoped:error
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = false:suggestion
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
csharp_prefer_simple_using_statement = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion

# Never include unnecessary using directives
dotnet_diagnostic.IDE0005.severity = error

# --- Naming conventions (coding-standards.md) ---

# Interfaces: PascalCase with 'I' prefix
dotnet_naming_rule.interfaces_should_be_i_prefixed.severity = error
dotnet_naming_rule.interfaces_should_be_i_prefixed.symbols = interface_symbol
dotnet_naming_rule.interfaces_should_be_i_prefixed.style = i_prefix_pascal_style

dotnet_naming_symbols.interface_symbol.applicable_kinds = interface
dotnet_naming_symbols.interface_symbol.applicable_accessibilities = *

dotnet_naming_style.i_prefix_pascal_style.capitalization = pascal_case
dotnet_naming_style.i_prefix_pascal_style.required_prefix = I

# Async methods: PascalCase with 'Async' suffix (checked before the general method rule)
dotnet_naming_rule.async_methods_should_be_async_suffixed.severity = error
dotnet_naming_rule.async_methods_should_be_async_suffixed.symbols = async_method_symbol
dotnet_naming_rule.async_methods_should_be_async_suffixed.style = async_suffix_pascal_style

dotnet_naming_symbols.async_method_symbol.applicable_kinds = method
dotnet_naming_symbols.async_method_symbol.required_modifiers = async
dotnet_naming_symbols.async_method_symbol.applicable_accessibilities = *

dotnet_naming_style.async_suffix_pascal_style.capitalization = pascal_case
dotnet_naming_style.async_suffix_pascal_style.required_suffix = Async

# Constant fields and private static readonly (constant-like): PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = error
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constant_symbol
dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case_style

dotnet_naming_symbols.constant_symbol.applicable_kinds = field
dotnet_naming_symbols.constant_symbol.required_modifiers = const
dotnet_naming_symbols.constant_symbol.applicable_accessibilities = *

dotnet_naming_rule.private_static_readonly_should_be_pascal_case.severity = error
dotnet_naming_rule.private_static_readonly_should_be_pascal_case.symbols = private_static_readonly_symbol
dotnet_naming_rule.private_static_readonly_should_be_pascal_case.style = pascal_case_style

dotnet_naming_symbols.private_static_readonly_symbol.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbol.required_modifiers = static,readonly
dotnet_naming_symbols.private_static_readonly_symbol.applicable_accessibilities = private

dotnet_naming_style.pascal_case_style.capitalization = pascal_case

# Other private fields: _camelCase
dotnet_naming_rule.private_fields_should_be_underscore_camel_case.severity = error
dotnet_naming_rule.private_fields_should_be_underscore_camel_case.symbols = private_field_symbol
dotnet_naming_rule.private_fields_should_be_underscore_camel_case.style = underscore_camel_case_style

dotnet_naming_symbols.private_field_symbol.applicable_kinds = field
dotnet_naming_symbols.private_field_symbol.applicable_accessibilities = private

dotnet_naming_style.underscore_camel_case_style.capitalization = camel_case
dotnet_naming_style.underscore_camel_case_style.required_prefix = _

# Types and remaining members (classes, structs, enums, methods, properties, events): PascalCase
dotnet_naming_rule.types_and_members_should_be_pascal_case.severity = error
dotnet_naming_rule.types_and_members_should_be_pascal_case.symbols = types_and_members_symbol
dotnet_naming_rule.types_and_members_should_be_pascal_case.style = pascal_case_style

dotnet_naming_symbols.types_and_members_symbol.applicable_kinds = class, struct, enum, property, method, event, delegate, type_parameter
dotnet_naming_symbols.types_and_members_symbol.applicable_accessibilities = *

# Locals and parameters: camelCase
dotnet_naming_rule.locals_and_parameters_should_be_camel_case.severity = suggestion
dotnet_naming_rule.locals_and_parameters_should_be_camel_case.symbols = locals_and_parameters_symbol
dotnet_naming_rule.locals_and_parameters_should_be_camel_case.style = camel_case_style

dotnet_naming_symbols.locals_and_parameters_symbol.applicable_kinds = parameter, local

dotnet_naming_style.camel_case_style.capitalization = camel_case
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches:
- master
- 'feature/**'
pull_request:
branches:
- master

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: Restore
run: dotnet restore

- name: Verify formatting
run: dotnet format --verify-no-changes

- 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,6 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp

# JetBrains Rider
.idea/
43 changes: 43 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this project is

SqlBound is a planned .NET/C# library providing SQLx-equivalent functionality (Rust's SQLx): **compile-time verified SQL queries** checked against a real database schema, reflection-free row materialization via Roslyn source generators, and SQL-file migrations. It will be published to NuGet.

Follow the design constraints below; they are deliberate decisions, not suggestions.

## Core design constraints

**Dapper coexistence is a hard requirement.** Both libraries must coexist cleanly in the same project, on the same `DbConnection` and `DbTransaction`, with zero conflict. Concretely:

- Build on `System.Data.Common` primitives; never own or wrap connections.
- Do NOT define extension methods on `IDbConnection`/`DbConnection` named `Query*`, `Execute*`, or `ExecuteScalar*` — those are Dapper's and would cause ambiguity errors when both namespaces are imported. Use generated static partial methods (the primary API) or distinct verbs (`FetchAsync`, `RunAsync`) / an instance-based `SqlSession` for the dynamic surface.
- No global static configuration state (Dapper's `SqlMapper` pattern) — conversions are resolved at compile time by the generator.
- SqlBound targets static, build-time-known queries; Dapper remains the tool for runtime-composed SQL. Don't add features that chase Dapper's niche.

**Verification architecture (the SQLx `query!` equivalent):**

- `[SqlQuery("...")]` attributes on `static partial` methods; an incremental source generator emits straight-line `DbDataReader` code (`GetInt32`, `IsDBNull`, …) — no reflection, no IL emit, Native AOT and trimming compatible.
- Database round-trips (prepare/describe against `SQLBOUND_DATABASE_URL`) happen only in the CLI `prepare` step or an opt-in MSBuild task — never inside the Roslyn analyzer. The in-IDE analyzer validates only against committed JSON snapshots in `.sqlbound/` (SQLx's offline `.sqlx` directory equivalent). This split is the load-bearing DX decision; there is an ADR planned for it.
- Diagnostic IDs use the `SQLB###` prefix.
- SQL Server is the pilot provider; then SQLite, Postgres, MySQL.

**Package layout (planned):**

- `SqlBound` — runtime core, `net8.0`+, dependency-free.
- `SqlBound.Generators` — generator + analyzer, `netstandard2.0` (Roslyn requirement), packed into `analyzers/dotnet/cs`, never a runtime dependency.
- `SqlBound.SqlServer` / `.Sqlite` / `.Npgsql` / `.MySql` — per-provider introspection and type mapping.
- `SqlBound.Cli` — dotnet tool: `prepare`, `verify`, `migrate add/run/revert`, `database create/drop`.

## Roadmap

Work proceeds milestone by milestone (M1–M16) across six phases: Bedrock (skeleton/CI, execution core, Dapper-coexistence sample), Codegen (materialization, query shapes, AOT + benchmarks), Verification (SQL Server introspection, diagnostics, offline snapshots), Providers, Migrations & CLI, and Ship (API freeze, NuGet 1.0). The Dapper-coexistence sample project (M3) doubles as a permanent CI regression test. Codegen precedes verification because the generator defines the shapes the verifier checks.

## Conventions
@.claude/rules/coding-standards.md
@.claude/rules/architecture.md
@.claude/rules/design-principles.md
@.claude/rules/testing.md
@.claude/rules/workflow.md
37 changes: 37 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project>

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<PropertyGroup Label="Package metadata">
<Authors>Luís Amorim</Authors>
<PackageVersion>0.1.0-preview.1</PackageVersion>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/lgamorim/sqlbound</PackageProjectUrl>
<RepositoryUrl>https://github.com/lgamorim/sqlbound</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<!--
Test projects are exempt from public XML-doc enforcement and are never packed (coding-standards.md).
GenerateDocumentationFile stays true even here: Roslyn requires it to run IDE0005 (unused usings)
as a build-time diagnostic (https://github.com/dotnet/roslyn/issues/41640); only the missing-doc-
comment warning (CS1591) itself is suppressed.
-->
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.UnitTests'))">
<NoWarn>$(NoWarn);CS1591</NoWarn>
<IsPackable>false</IsPackable>
</PropertyGroup>

</Project>
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2026 Luís Amorim

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
Loading
Loading