Skip to content

Make Database mutators non-const and Instance() return a writable handle#36

Merged
jkalias merged 1 commit into
mainfrom
claude/const-correct-database
Jul 18, 2026
Merged

Make Database mutators non-const and Instance() return a writable handle#36
jkalias merged 1 commit into
mainfrom
claude/const-correct-database

Conversation

@jkalias

@jkalias jkalias commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #26.

The smell: Database::Instance() returned std::shared_ptr<const Database>, yet every mutating operation — Save, SaveAutoIncrement, Update, Delete (all overloads), and UnsafeSql — was declared const. A handle typed as pointer-to-const could freely write to the database. It compiled because the connection is a raw sqlite3* db_ (const doesn't propagate through the pointer to the SQLite handle) and db_mutex_ is mutable. The API misrepresented intent: const looked read-only but wasn't, which can mislead callers reasoning about thread-safety and aliasing.

Fix

Make const mean read-only:

  • Instance() now returns std::shared_ptr<Database>.
  • The write methods (Save/SaveAutoIncrement/Update/Delete overloads, UnsafeSql) and their private void* worker helpers are now non-const.
  • The genuine reads (FetchAll, Fetch, private FetchRecords) stay const, and db_mutex_ stays mutable so they can still lock it.

Ownership/lifecycle model unchanged. The alive-across-Finalize() guarantee comes from Instance() returning a strong shared_ptr, not from the pointee being const — serialized access via db_mutex_, the retired_ reinitialization guard, and all CRUD semantics are untouched. The concurrency/lifecycle tests pass unchanged.

Read-only views still available. The implicit shared_ptr<Database>shared_ptr<const Database> conversion yields a view through which only the const fetch methods compile; the README's ownership section documents this. Existing call sites of the form const auto db = Database::Instance(); db->Save(...) still compile (const handle, non-const pointee), so no call-site churn.

Tests

  • Compile-time SFINAE checks in tests/database_test.cc assert the new contract: Save is not invocable through const Database, FetchAll is. The trait machinery was separately verified to detect the positive case on non-const Database, so the negative assert isn't vacuously passing.
  • New runtime test ReadOnlyHandleCanStillFetch: a shared_ptr<const Database> view can still fetch.
  • Full suite: 76/76 tests pass in both C++11 and C++20 locally (Linux/GCC), including concurrency_test.cc (lifecycle/threading guarantees) and all of database_test.cc unchanged.

ABI note

This is an ABI change: method const-ness and Instance()'s return type alter mangled names. Acceptable for a source library, but downstream binaries built against the old headers need a rebuild.

Scope

Const-correctness only — no behavior change to CRUD semantics, and no other open design issues (#2 nullable fields, #14 identifier quoting, etc.) folded in.


Generated by Claude Code

…dle (#26)

Instance() returned std::shared_ptr<const Database>, yet Save,
SaveAutoIncrement, Update, Delete, and UnsafeSql were all declared const -
so a handle typed as pointer-to-const could freely mutate persistent state.
It compiled because the connection is a raw sqlite3* (const does not
propagate through the pointer) and db_mutex_ is mutable. The API
misrepresented intent: const looked read-only but wasn't.

Make const mean read-only:
- Instance() now returns std::shared_ptr<Database>.
- The write methods (Save/SaveAutoIncrement/Update/Delete overloads,
  UnsafeSql) and their private void* worker helpers are now non-const.
- The genuine reads (FetchAll, Fetch, private FetchRecords) stay const, and
  db_mutex_ stays mutable so they can still lock it.

The ownership/lifecycle model is unchanged: the alive-across-Finalize()
guarantee comes from Instance() returning a strong shared_ptr, not from the
pointee being const. Serialized access via db_mutex_, the retired_
reinitialization guard, and all CRUD semantics are untouched. A read-only
view is still available via the implicit shared_ptr<Database> ->
shared_ptr<const Database> conversion, through which only the const fetch
methods compile; README updated accordingly.

Existing call sites of the form `const auto db = Database::Instance()`
still compile (const handle, non-const pointee).

Tests: compile-time SFINAE checks in database_test.cc assert the new
contract (Save is NOT invocable through const Database, FetchAll IS), with
the trait machinery verified to detect the positive case on non-const
Database so the negative assert isn't vacuous; plus a runtime test that a
shared_ptr<const Database> view can still fetch. Full suite: 76/76 pass in
both C++11 and C++20 (including the concurrency/lifecycle tests).

Note this is an ABI change (method const-ness and Instance()'s return type
alter mangled names) - acceptable for a source library.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK
@jkalias
jkalias merged commit f53139f into main Jul 18, 2026
15 checks passed
@jkalias
jkalias deleted the claude/const-correct-database branch July 18, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mutating operations are const methods on Database (const-correctness lie)

2 participants