# Upgrading from cached 1.1 to 2.0

cached 2.0 is a major release that bundles two sets of changes: a new set of sharded
concurrent stores and the `#[concurrent_cached]` macro rewrite (previously staged as 1.2),
plus a full unification of `Option`/`Result` implicit handling across all three macros.

## Before you upgrade: toolchain requirement

cached 2.0 **requires Rust ≥ 1.85** (up from 1.80) and uses the **2024 edition**. Edition
2024 was stabilized in Rust 1.85, so that is the new minimum supported Rust version. If you
are on Rust 1.80–1.84, update your toolchain before upgrading — otherwise you will get
edition/compile errors with no obvious pointer to the cause.

You do **not** need the latest stable: any Rust ≥ 1.85 works. (The repository pins a newer
stable in `rust-toolchain.toml`, but that pin is for contributors/CI only — it is not
published and does not propagate to your project.)

## Quick summary

**Macro changes (most impactful):**
- Remove `result = true` and `option = true` everywhere — they are now the default
- `result_fallback = true` no longer needs `result = true` alongside it
- `#[concurrent_cached]` no longer needs `ty`/`create`/`map_error` for basic in-memory use

**Store behavior changes (rare impact):**
- `cache_remove` on expiring stores returns `None` for expired-but-present entries
- `LruCache::retain` now fires `on_evict` for removed entries

**Builder-only construction and renames:**
- All store-returning constructors (`new`, `with_capacity`, `with_ttl`, `with_max_size`, `with_shards`, etc.) are **removed**. Every store is now constructed via `X::builder().…setters….build()?`
- `build()` now returns `Result<Store, BuildError>` for all in-memory and sharded stores — add `?` or `.unwrap()` to every call
- `try_build()` is removed; use `build()` instead
- The size-bound builder setter `.size(n)` is renamed to `.max_size(n)`. The `#[cached(size = N)]` / `#[concurrent_cached(size = N)]` macro attribute is likewise renamed to `max_size = N` — the old `size = N` spelling still works but now emits a deprecation warning
- Zero TTL is now always rejected — `builder().ttl(Duration::ZERO).build()` returns `Err(BuildError::InvalidTtl)`

---

## Step-by-step upgrade

### 1. Remove `result = true` and `option = true`

These attributes are no longer accepted. The behavior they enabled is now the default.

**Before:**
```rust
#[cached(result = true)]
fn load(id: u64) -> Result<Row, DbError> { ... }

#[cached(option = true)]
fn find(id: u64) -> Option<User> { ... }

#[once(result = true)]
fn app_config() -> Result<Config, Error> { ... }

#[once(option = true, ttl = 60)]
fn feature_flag() -> Option<Flag> { ... }

#[cached(result = true, result_fallback = true, ttl = 60)]
fn fetch(id: u64) -> Result<Data, Error> { ... }
```

**After:**
```rust
#[cached]
fn load(id: u64) -> Result<Row, DbError> { ... }

#[cached]
fn find(id: u64) -> Option<User> { ... }

#[once]
fn app_config() -> Result<Config, Error> { ... }

#[once(ttl = 60)]
fn feature_flag() -> Option<Flag> { ... }

#[cached(result_fallback = true, ttl = 60)]
fn fetch(id: u64) -> Result<Data, Error> { ... }
```

### 2. `#[concurrent_cached]` now accepts `Option<T>` and plain return types

In 1.x, `#[concurrent_cached]` only supported `Result<T, E>` returns. In 2.0, `Option<T>` returns
and plain `T: Clone` returns are supported on the default in-memory sharded path — `None` is not
cached by default.

**Note:** `option = true` was never a recognized attribute on `#[concurrent_cached]` — it was
silently ignored by the macro parser in 1.x. There is nothing to migrate here. If you have
`option = true` on a `#[concurrent_cached]` annotation, remove it; it had no effect.

**After (new, works in 2.0):**
```rust
#[concurrent_cached]
fn find(id: u64) -> Option<Row> { ... }  // None is not cached by default
```

### 3. Opt-in to the old behavior (if needed)

If you previously had a plain `#[cached]` or `#[once]` on a `Result` return and relied on `Err` being cached:
```rust
#[cached(cache_err = true)]
fn load(id: u64) -> Result<Row, DbError> { ... }

#[once(cache_err = true)]
fn load_global() -> Result<Row, DbError> { ... }
```

If you previously had a plain `#[cached]` or `#[once]` on an `Option` return and relied on `None` being cached:
```rust
#[cached(cache_none = true)]
fn find(id: u64) -> Option<User> { ... }

#[once(cache_none = true)]
fn maybe_global() -> Option<User> { ... }
```

### 4. Fix custom cache types that store `Option<T>` or `Result<T,E>`

If your custom `ty` holds `Option<T>` or `Result<T,E>` as the value type, add the opt-in:

```rust
// Before:
#[cached(ty = "TtlCache<String, Option<String>>", create = "{ ... }")]
fn fetch(key: String) -> Option<String> { ... }

// After:
#[cached(ty = "TtlCache<String, Option<String>>", create = "{ ... }", cache_none = true)]
fn fetch(key: String) -> Option<String> { ... }
```

### 5. Update custom `Cached` / `ConcurrentCached` / `ConcurrentCachedAsync` implementations

If you maintain a custom store that manually implements any of these traits, 2.0 adds a new **required method** — `cache_remove_entry` — that returns both the stored key and value when an entry is removed:

- `Cached<K, V>` gains `fn cache_remove_entry<Q>(&mut self, k: &Q) -> Option<(K, V)>`
- `ConcurrentCached<K, V>` gains `fn cache_remove_entry(&self, k: &K) -> Result<Option<(K, V)>, Self::Error>`
- `ConcurrentCachedAsync<K, V>` gains the async equivalent

The default `cache_delete` method (new in 2.0) is implemented on top of `cache_remove_entry`, so you only need to add `cache_remove_entry` to your impl:

```rust
impl Cached<String, MyValue> for MyStore {
    // ... existing methods ...

    fn cache_remove_entry<Q>(&mut self, k: &Q) -> Option<(String, MyValue)>
    where
        String: std::borrow::Borrow<Q>,
        Q: std::hash::Hash + Eq + ?Sized,
    {
        self.inner.remove_entry(k)
        // remove_entry returns the *stored* key, not a reconstruction from the lookup key
    }
}
```

Two more changes affect **custom `ConcurrentCached` / `ConcurrentCachedAsync` implementations** (the single-owner `Cached` trait is unaffected):

- **The TTL/refresh mutators now take `&self`, not `&mut self`.** `set_refresh_on_hit`, `set_ttl`, and `unset_ttl` changed receiver so you can adjust them on a shared store (behind an `Arc` or in a `static`), like the rest of these `&self`-contract traits. Change the receiver in your impl and move the mutated state behind interior mutability — the bundled `DiskCache` / `RedisCache` / `AsyncRedisCache` impls use `parking_lot::Mutex` for the TTL and an `AtomicBool` for the refresh flag:

  ```rust
  // before: fn set_ttl(&mut self, ttl: Duration) -> Option<Duration>
  fn set_ttl(&self, ttl: Duration) -> Option<Duration> {
      let mut guard = self.ttl.lock();          // self.ttl: Mutex<Option<Duration>>
      guard.replace(ttl)
  }
  ```

- **A new `cache_size` method was added**, but it has a default of `Ok(None)`, so existing impls keep compiling with no change. Override it only if your store can report an entry count cheaply:

  ```rust
  fn cache_size(&self) -> Result<Option<usize>, Self::Error> {
      Ok(Some(self.len()))
  }
  ```

### 6. Switch to builder-only construction; rename `.size()` to `.max_size()`

All direct, store-returning constructors are **removed** in 2.0. Every store must now be
constructed through its builder. `build()` returns `Result<Store, BuildError>` — add `?` or
`.unwrap()` at every call site. The redundant `try_build()` alias is also removed; use
`build()` instead.

```rust
// Before (1.1):
let c = LruCache::with_max_size(100);
let t = TtlCache::with_ttl(ttl);
let lt = LruTtlCache::with_max_size_and_ttl(100, ttl);
let u = UnboundCache::new();
let sc = ShardedLruCache::with_max_size(500);

// After (2.0):
let c  = LruCache::builder().max_size(100).build()?;
let t  = TtlCache::builder().ttl(ttl).build()?;          // build() now returns Result
let lt = LruTtlCache::builder().max_size(100).ttl(ttl).build()?;
let u  = UnboundCache::builder().build()?;
let sc = ShardedLruCache::builder().max_size(500).build()?;
```

The full removal list spans all in-memory and sharded stores. See the table below for the
most common patterns:

| Removed constructor | Builder replacement |
| --- | --- |
| `UnboundCache::new()` | `UnboundCache::builder().build()?` |
| `UnboundCache::with_capacity(n)` | `UnboundCache::builder().capacity(n).build()?` |
| `LruCache::with_max_size(n)` | `LruCache::builder().max_size(n).build()?` |
| `TtlCache::with_ttl(t)` | `TtlCache::builder().ttl(t).build()?` |
| `TtlCache::with_ttl_and_capacity(t, n)` | `TtlCache::builder().ttl(t).capacity(n).build()?` |
| `TtlCache::with_ttl_and_refresh(t, r)` | `TtlCache::builder().ttl(t).refresh_on_hit(r).build()?` |
| `LruTtlCache::with_max_size_and_ttl(n, t)` | `LruTtlCache::builder().max_size(n).ttl(t).build()?` |
| `TtlSortedCache::new(t)` | `TtlSortedCache::builder().ttl(t).build()?` |
| `ExpiringLruCache::with_max_size(n)` | `ExpiringLruCache::builder().max_size(n).build()?` |
| `ShardedCache::new()` / `with_shards(s)` | `ShardedCache::builder()[.shards(s)].build()?` |
| `ShardedLruCache::with_max_size(n)` | `ShardedLruCache::builder().max_size(n).build()?` |
| `ShardedTtlCache::with_ttl(t)` | `ShardedTtlCache::builder().ttl(t).build()?` |

The `Sharded*` rows are **new types in 2.0** — they had no 1.1 equivalent, so you won't find
their `new()` / `with_shards()` calls anywhere in a 1.1 codebase. They're listed only to show
that, like every other store, they're builder-only; the left-column constructors never shipped.

**`DiskCache` / `RedisCache` / `AsyncRedisCache` are unaffected** — their `new(...)` /
`builder(...)` already returned a builder and `build()` already returned `Result`. Only
their redundant `try_build()` alias is removed.

**Zero TTL is now rejected.** `builder().ttl(Duration::ZERO).build()` returns
`Err(BuildError::InvalidTtl { ttl })`. The old permissive path that treated a zero TTL as
"everything expires immediately" no longer exists. If you previously relied on this, either
skip caching that code path entirely, or use a very small non-zero TTL (e.g.
`Duration::from_nanos(1)`) if entries must be treated as expired on the very next access.

**Existing `builder()` call sites on in-memory stores** that already used the builder pattern
now need `?` or `.unwrap()` because `build()` returns `Result` instead of the store directly:

```rust
// Before (1.1) — build() returned the store:
let c = LruCache::builder().max_size(100).build();

// After (2.0) — build() returns Result<Store, BuildError>:
let c = LruCache::builder().max_size(100).build()?;
```

**The `.size(n)` builder setter is renamed to `.max_size(n)`** to make clear it sets the
eviction bound, not a current count. This affects all LRU-family builders and `TtlSortedCache`.

```rust
// Before (1.1) — `.size()` was the eviction-bound setter, renamed in 2.0:
let c = LruCache::builder().size(100).build();

// After (2.0):
let c = LruCache::builder().max_size(100).build()?;
```

The `#[cached(size = N)]` / `#[concurrent_cached(size = N)]` macro attribute is **also renamed
to `max_size = N`** (see "New in 2.0" below); the old `size = N` spelling keeps working but now
emits a deprecation warning.

#### 6a. `TtlSortedCache` builder and runtime setters

`TtlSortedCache` is now also builder-only. Its builder gains a dedicated `.capacity(n)` setter
for the preallocation hint, distinct from `.max_size(n)` (the eviction bound). The runtime
size-limit setters are also renamed:

```rust
// Before (1.1):
let mut c = TtlSortedCache::new(ttl);
let mut c = TtlSortedCache::with_ttl_and_capacity(ttl, 100);  // 100 is a preallocation hint
c.size_limit(50);
c.try_size_limit(50)?;

// After (2.0):
let mut c = TtlSortedCache::builder().ttl(ttl).build()?;
let mut c = TtlSortedCache::builder().ttl(ttl).capacity(100).build()?;  // preallocation hint
c.set_max_size(50);                                                       // runtime eviction bound
c.try_set_max_size(50)?;
```

---

## New in 2.0

### `#[concurrent_cached]` defaults to in-memory sharded stores

You no longer need `ty`/`create`/`map_error` for basic in-memory caching:

```rust
#[concurrent_cached]                           // unbounded
#[concurrent_cached(max_size = 1000)]          // LRU-bounded
#[concurrent_cached(ttl = 60)]                 // TTL-expiring (time_stores feature)
#[concurrent_cached(max_size = 500, ttl = 30)] // LRU + TTL
#[concurrent_cached(expires = true)]           // per-value expiry (Expires trait)
```

### `result_fallback` on `#[concurrent_cached]`

```rust
#[concurrent_cached(ttl = 60, result_fallback = true)]
fn load(id: u64) -> Result<Row, DbError> {
    // On Err, last cached Ok for this key is returned instead
}
```

### New sharded stores available directly

`ShardedCache`, `ShardedLruCache`, `ShardedTtlCache`, `ShardedLruTtlCache`,
`ShardedExpiringCache`, `ShardedExpiringLruCache` — all `Arc`-backed, `Send + Sync`,
with builder APIs, `on_evict` callbacks, and `metrics()`.

### `capacity()` getter on the LRU-family stores

`LruCache`, `LruTtlCache`, and `ExpiringLruCache` — and their sharded counterparts
`ShardedLruCache`, `ShardedLruTtlCache`, and `ShardedExpiringLruCache` — expose an inherent
`capacity()` — distinct from `cache_size()`, which returns the current live entry count.

For the non-sharded stores, `capacity()` returns the exact value passed to `.max_size(n)`. For
the sharded stores, `capacity()` returns the **effective** total across all shards, which may
exceed the requested `max_size` because per-shard capacity is rounded up with ceiling division
(e.g. `builder().max_size(1000).build()?` → `capacity() == 1008` with the default 16 shards).

### `max_size = N` macro attribute (replaces `size = N`)

`#[cached]` and `#[concurrent_cached]` now prefer `max_size = N`, matching the renamed `max_size`
builder methods. The old `size = N` spelling is a **deprecated alias** — it still compiles but
emits a deprecation warning steering you to `max_size`. Setting both on the same annotation is a
compile error. To migrate, rename the attribute:

```rust
#[cached(max_size = 100)]
fn f(x: u64) -> u64 { x * 2 }

#[concurrent_cached(max_size = 100)]
fn g(x: u64) -> u64 { x * 2 }
```

---

## Store behavior changes

### `cache_remove` on expiring stores

`ExpiringCache`, `ExpiringLruCache`, and sharded expiring stores now return `None` when removing
an entry that is present but already expired (previously returned `Some(value)`). The entry is
still removed; `on_evict` still fires. This is the same behavior as the non-expiring TTL stores.

**Impact on `cache_delete`:** `cache_delete` now returns `true` whenever an entry is
physically removed, including expired entries. If you need to distinguish a live removal
from an expired one, use `cache_remove`; if you need to distinguish an expired-but-present
entry from an absent key, use `cache_remove_entry`.

### `LruCache::retain` fires `on_evict`

Each entry removed by `retain` now fires `on_evict` (if registered) and increments
`cache_evictions()`. Previously `retain` was side-effect-free.

### All non-unbound stores fire `on_evict` on explicit `cache_remove`

Previously only `UnboundCache` fired `on_evict` on an explicit `cache_remove`; now every
in-memory store does (`LruCache`, `TtlCache`, `LruTtlCache`, `ExpiringCache`,
`ExpiringLruCache`, …). If your `on_evict` callback does resource cleanup (closing a file
handle, decrementing a refcount), make sure it is safe to run for entries you remove
explicitly — not just for entries the cache evicts on its own.

---

## Cargo.toml

```toml
# requires Rust ≥ 1.85 (MSRV raised from 1.80; cached 2.0 uses the 2024 edition)
cached = "2.0"
# if pinning the proc-macro directly:
# cached_proc_macro = "2.0"
```
