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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ caches the result, and records which schemas appear in it for later eviction.
MyApp.Users.find(%{id: 5})
end)

# Cache a collection (prefix with "all_" for write-through support)
{:ok, users} =
SchemaCache.read("all_active_users", %{active: true}, :timer.minutes(5), fn ->
{:ok, MyApp.Users.all(%{active: true})}
# Cache a collection
users =
SchemaCache.read("users", %{active: true}, :timer.minutes(5), fn ->
MyApp.Users.all(%{active: true})
end)
```

Expand Down
6 changes: 3 additions & 3 deletions guides/explanation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ SchemaCache.update(fn -> update_user(user, params) end, strategy: :write_through
| <- 7 -- {:ok, updated} | |
| | |

For collection keys (prefixed with "all_"), SchemaCache
finds the specific item within the list by primary key
and replaces it in place.
For collections (detected by the cached value being a list),
SchemaCache finds the specific item within the list by
primary key and replaces it in place.
```

## Eviction Flow
Expand Down
27 changes: 10 additions & 17 deletions guides/tutorials/basic_operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ The first call executes the callback and caches the result. Subsequent calls wit

### Caching a Collection

Prefix collection keys with `"all_"`. This convention is required for write-through to distinguish collections from singular values.

```elixir
{:ok, users} =
SchemaCache.read("all_active_users", %{active: true}, :timer.minutes(5), fn ->
{:ok, MyApp.Users.all(%{active: true})}
users =
SchemaCache.read("users", %{active: true}, :timer.minutes(5), fn ->
MyApp.Users.all(%{active: true})
end)
```

Expand Down Expand Up @@ -96,23 +94,18 @@ The TTL argument is passed to the adapter when re-storing reference sets (if app

## Key Conventions

Cache keys are composed from two parts: a name and params map. The name determines the cache key prefix, and the params are deterministically serialized into the key.

### The `"all_"` Prefix

Keys prefixed with `"all_"` are treated as collection keys. This distinction matters for:

- **`create/1`**: Only evicts collection keys (prefixed with `"all_"`) for the schema type.
- **Write-through**: Updates items within lists for collection keys, replaces the entire value for singular keys.
Cache keys are composed from two parts: a name and params map. The name identifies the domain (e.g. `"users"`, `"jobs"`), and the params are deterministically serialized into the key.

```elixir
# Singular (no prefix)
SchemaCache.read("find_user", %{id: 5}, ttl, fn -> ... end)
# Singular
SchemaCache.read("users", %{id: 5}, ttl, fn -> ... end)

# Collection ("all_" prefix required)
SchemaCache.read("all_users", %{active: true}, ttl, fn -> ... end)
# Collection
SchemaCache.read("users", %{active: true}, ttl, fn -> ... end)
```

Write-through automatically distinguishes collections from singular values based on the cached data type. No naming convention required.

### Params Determinism

Params maps are serialized deterministically using sorted keys and Jason encoding. This means `%{a: 1, b: 2}` and `%{b: 2, a: 1}` produce the same cache key.
Expand Down
27 changes: 9 additions & 18 deletions lib/schema_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -280,27 +280,19 @@ defmodule SchemaCache do
end
end

defp update_key_ref(
"all_" <> _ = key_ref,
ttl,
%_{} = value
) do
defp update_key_ref(key_ref, ttl, value) do
case Adapter.get(adapter(), key_ref) do
{:ok, nil} ->
:ok

{:ok, cached_collection} when is_list(cached_collection) ->
maybe_update_cached_collection(cached_collection, key_ref, ttl, value)
{:ok, cached} when is_list(cached) ->
maybe_update_cached_collection(cached, key_ref, ttl, value)

_ ->
:ok
{:ok, _singular} ->
Adapter.put(adapter(), key_ref, value, ttl)
end
end

defp update_key_ref(key_ref, ttl, value) do
Adapter.put(adapter(), key_ref, value, ttl)
end

defp maybe_update_cached_collection(
cached_collection,
key_ref,
Expand Down Expand Up @@ -350,9 +342,8 @@ defmodule SchemaCache do

## Arguments

* `key` - A string prefix identifying the query. Use `"all_"` prefix
for collections so write-through can distinguish them from singular
values.
* `key` - A string prefix identifying the query (e.g. `"users"`,
`"jobs"`). Combined with `params` to form the full cache key.
* `params` - A map of query parameters. Combined with `key` to form
the full cache key.
* `ttl` - Time-to-live passed through to the adapter. Pass `nil` to
Expand All @@ -375,13 +366,13 @@ defmodule SchemaCache do

# Singular record
{:ok, user} =
SchemaCache.read("find_user", %{id: 5}, :timer.minutes(15), fn ->
SchemaCache.read("users", %{id: 5}, :timer.minutes(15), fn ->
MyApp.Users.find(%{id: 5})
end)

# Collection
users =
SchemaCache.read("all_users", %{active: true}, :timer.minutes(5), fn ->
SchemaCache.read("users", %{active: true}, :timer.minutes(5), fn ->
MyApp.Users.all(%{active: true})
end)
"""
Expand Down
8 changes: 4 additions & 4 deletions lib/schema_cache/key_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ defmodule SchemaCache.KeyGenerator do

## Examples

iex> SchemaCache.KeyGenerator.cache_key("find_user", %{id: 5})
"find_user:{\"id\":5}"
iex> SchemaCache.KeyGenerator.cache_key("users", %{id: 5})
"users:{\"id\":5}"

iex> SchemaCache.KeyGenerator.cache_key("foo", %{order_by: [fiz: "buz"]})
"foo:{\"order_by\":{\"fiz\":\"buz\"}}"

iex> SchemaCache.KeyGenerator.cache_key("all_users", %{})
"all_users:{}"
iex> SchemaCache.KeyGenerator.cache_key("users", %{})
"users:{}"

"""
@spec cache_key(String.t(), map()) :: String.t()
Expand Down
6 changes: 3 additions & 3 deletions test/schema_cache/key_generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ defmodule SchemaCache.KeyGeneratorTest do

describe "cache_key/2" do
test "generates a key from prefix and params" do
assert "find_user:{\"id\":5}" = KeyGenerator.cache_key("find_user", %{id: 5})
assert "users:{\"id\":5}" = KeyGenerator.cache_key("users", %{id: 5})
end

test "converts order_by keyword list to map for encoding" do
key = KeyGenerator.cache_key("all_users", %{order_by: [name: :asc]})
key = KeyGenerator.cache_key("users", %{order_by: [name: :asc]})
assert key =~ "order_by"
assert key =~ "name"
end

test "handles empty params" do
assert "all_users:{}" = KeyGenerator.cache_key("all_users", %{})
assert "users:{}" = KeyGenerator.cache_key("users", %{})
end
end

Expand Down
Loading