|
| 1 | +# C++ guidelines (self-contained) |
| 2 | + |
| 3 | +This file is the canonical, project-internal C++ ruleset. Contributors and agents do **not** need any external file (no `~/.claude/rules/cpp.md`, no global notes) — every rule that applies to Lightweight is here. |
| 4 | + |
| 5 | +## 1. General C++23 baseline |
| 6 | + |
| 7 | +### Design |
| 8 | +- **Data-driven design** — avoid hard-coded magic values. Prefer descriptor tables, configuration, or polymorphic dispatch over scattered conditionals. |
| 9 | +- **Dependency injection** — pass collaborators through constructors / parameters so tests can substitute fakes. Anything touching I/O, time, randomness, or the database must be injectable. |
| 10 | + |
| 11 | +### Documentation |
| 12 | +- **Doxygen** on every new public function, parameter, return, class, struct, and member: |
| 13 | + ```cpp |
| 14 | + /// Short description. |
| 15 | + /// @param name Description. |
| 16 | + /// @return Description. |
| 17 | + ``` |
| 18 | +- Be factual — no marketing prose. |
| 19 | + |
| 20 | +### Type & const correctness |
| 21 | +- **`const`-correctness** throughout: refs, pointers, member functions, parameters that don't mutate. |
| 22 | +- **`auto` type deduction** for readability; **structured bindings** for tuple-like returns. |
| 23 | +- Mark return values **`[[nodiscard]]`** where ignoring the result would be a bug — this includes builders, query objects, and anything returning `std::expected`. |
| 24 | + |
| 25 | +### Modern C++ surface |
| 26 | +- Prefer **C++23**: `constexpr`, `std::ranges`, `std::format`, `std::expected`, `std::span`. |
| 27 | +- **`std::expected<T, E>`** with monadic chaining (`and_then`, `or_else`, `transform`, `transform_error`) for fallible operations. Avoid nested `if`s when a chain reads better. |
| 28 | +- **`std::span`** for arrays / contiguous sequences in API surfaces. |
| 29 | +- **Range views** for generation/transformation: `std::views::iota`, `std::views::filter`, `std::views::transform`, etc. |
| 30 | + |
| 31 | +### Forbidden constructs |
| 32 | +- **C-style loops are forbidden.** Use range-based `for` and the views above. |
| 33 | +- **No raw owning pointers.** `std::unique_ptr` / `std::shared_ptr` for ownership, RAII for resources. |
| 34 | +- **No `NOLINT` suppressions** — fix the underlying issue clang-tidy reports. The `linux-clang-debug` preset enables `clang-tidy` automatically. |
| 35 | +- **No new third-party dependencies** without strong justification — vcpkg manifest at `vcpkg.json` lists what we already accept. |
| 36 | + |
| 37 | +### Tooling |
| 38 | +- Run **`clang-format`** on every changed file (project `.clang-format` is authoritative). |
| 39 | +- Build with the matching preset (`linux-clang-debug` / `windows-clangcl-debug`); resolve every warning — `PEDANTIC_COMPILER_WERROR=ON`. |
| 40 | +- All changes must be covered by unit tests; aim to **increase** code coverage with every PR. |
| 41 | + |
| 42 | +## 2. Lightweight-specific patterns |
| 43 | + |
| 44 | +### Namespace & symbol visibility |
| 45 | +- All public symbols live in the `Lightweight` namespace; the alias `Light` is provided for brevity in user code. |
| 46 | +- Public headers must be **self-contained** — they must compile with no PCH and only their own includes. |
| 47 | +- Keep ABI-affecting changes deliberate; the library is consumed as a vcpkg port. |
| 48 | + |
| 49 | +### Error handling on the public API |
| 50 | +Prefer `std::expected<T, SqlError>` for fallible public APIs. Reserve exceptions for programmer errors (precondition violation, contract misuse). Internally, ODBC `SQLRETURN` codes are wrapped via the helpers in `src/Lightweight/SqlError.{hpp,cpp}` and `SqlErrorDetection.hpp`. |
| 51 | + |
| 52 | +```cpp |
| 53 | +return statement.Prepare(sql) |
| 54 | + .and_then([&] { return statement.Execute(args...); }) |
| 55 | + .transform([&](auto&&) { return …; }) |
| 56 | + .transform_error([](SqlError const& e) { return wrapWithContext(e); }); |
| 57 | +``` |
| 58 | +
|
| 59 | +### ODBC `SQLHANDLE` lifetime |
| 60 | +ODBC handles (env, dbc, stmt) are owned by `SqlConnection` / `SqlStatement` (and the small RAII wrappers near them). They are released in the destructor. Never: |
| 61 | +- allocate a raw `SQLHANDLE` at call sites, |
| 62 | +- store one beyond the lifetime of its wrapper, |
| 63 | +- pass `SQLHANDLE` across abstraction boundaries (pass the wrapper instead). |
| 64 | +
|
| 65 | +### Per-DBMS dispatch |
| 66 | +Per-DBMS branching belongs only inside `SqlQueryFormatter` and its subclasses. Business logic must call a virtual method on the formatter and let the override decide. **Do not** write `if (server == SqlServerType::POSTGRESQL)` outside `QueryFormatter/`. |
| 67 | +
|
| 68 | +If a new feature behaves differently on each DBMS: |
| 69 | +1. Add a `[[nodiscard]] virtual` method to `SqlQueryFormatter` with a sensible default. |
| 70 | +2. Override per DBMS in `PostgreSqlFormatter`, `SQLiteFormatter`, `SqlServerFormatter`. |
| 71 | +3. Cover all three in tests. |
| 72 | +
|
| 73 | +### `SqlDataBinder<T>` contract for Unicode strings |
| 74 | +Unicode-bearing string types must round-trip identically across MSSQL, PostgreSQL, and SQLite. This is non-trivial because each driver has different opinions about UTF-16 vs UTF-8. |
| 75 | +
|
| 76 | +The repo's hard-won rules (codified in commits `894c67c7`, `89885982`, `f311cb9f`): |
| 77 | +
|
| 78 | +- Bind Unicode payloads via **`SQL_C_WCHAR`** (UTF-16) uniformly across the `BasicStringBinder` surface — `BasicStringBinder.hpp` does this. |
| 79 | +- For non-`u16string` Unicode types, transcode through a temporary `std::u16string` using `UnicodeConverter.hpp`, bind that, and copy back in the post-process callback (`SqlDataBinderCallback::PlanPostProcessOutputColumn`). |
| 80 | +- Use `SQL_C_WCHAR` even where the driver "would also accept" `SQL_C_CHAR` — the latter is unreliable for non-ASCII on `psqlODBC`. |
| 81 | +- For PostgreSQL connection strings, **disable LF↔CRLF translation** (`LFConversion=0`); without this, embedded newlines in character data silently mutate. |
| 82 | +
|
| 83 | +### `[[nodiscard]]` policy on builders |
| 84 | +Query builders (`SqlQuery::Select`, `Insert`, `Update`, `Delete`, `Migrate`) and `DataMapper::Query<…>` return objects that *must* be terminated (`.All()`, `.First()`, `.Execute()`, etc.). Mark every step `[[nodiscard]]` so accidental discard is a build error. |
| 85 | +
|
| 86 | +### Catch2 + DI in tests |
| 87 | +Tests must obtain a connection through the test fixture wired to `--test-env=<name>`, never by constructing one with hard-coded strings. New connection strings go in `.test-env.yml`. See `.agent/testing.md`. |
| 88 | +
|
| 89 | +### Reflection |
| 90 | +The library uses C++20 member pointers by default and supports a C++26 reflection mode (`LIGHTWEIGHT_CXX26_REFLECTION`). New code that enumerates record fields should use the abstraction (e.g., the `Member(x)` macro pattern in `src/tests/Utils.hpp`) rather than committing to one mode. |
| 91 | +
|
| 92 | +### Modules |
| 93 | +`Lightweight.cppm` is the C++20 module aggregator (`LIGHTWEIGHT_BUILD_MODULES=ON`, requires CMake ≥ 3.28). New public headers must be addable to the module export list without breaking the build. |
| 94 | +
|
| 95 | +## 3. Quick checklist before pushing |
| 96 | +
|
| 97 | +- `clang-format` clean |
| 98 | +- `clang-tidy` clean (no `NOLINT`s added) |
| 99 | +- `linux-clang-debug` builds with no warnings |
| 100 | +- Tests run green against `sqlite3`, `mssql2022`, `postgres` |
| 101 | +- New public APIs have doxygen + `[[nodiscard]]` where relevant |
| 102 | +- No new `if (server == …)` outside `QueryFormatter/` |
| 103 | +- New SQL primitives go through `SqlQueryFormatter` virtuals |
| 104 | +- Unicode-bearing types round-trip via `SQL_C_WCHAR` / `BasicStringBinder.hpp` |
| 105 | +- `/simplify` was run; out-of-scope findings were either addressed or surfaced to the user |
0 commit comments