Skip to content
Open
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
33 changes: 33 additions & 0 deletions implementation_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Implementation Plan: Sandbox Adapter Parity

## Goal
Make `Cache.Sandbox` behave as a drop-in replacement for Redis/ETS/DETS/ConCache adapters so all adapter-specific tests pass when `sandbox?` is enabled.

## Discoveries (Audit)
- Redis JSON operations in sandbox return `:ok` instead of `{:ok, count}` for `json_delete/2`, `json_incr/3`, `json_clear/3`, `json_array_append/4`.
- Redis hash operations in sandbox return `:ok` for `hash_set/3`, `hash_delete/2`, and `hash_set_many/1` when adapter tests expect `{:ok, count}` or `{:ok, [counts]}` with TTL.
- ETS `info/0` in sandbox does not include `:name` which ETS tests require.
- ETS/DETS conversion APIs (`to_dets`, `from_dets`, `to_ets`, `from_ets`) return `{:error, :not_supported_in_sandbox}` but tests expect working conversions.
- ETS/DETS macros bypass sandbox because they call `:ets`/`:dets` directly even when `sandbox?` is enabled.
- ConCache-specific APIs (`get_or_store/3`, `dirty_get_or_store/2`) are missing from sandbox but tests call them.

## Plan
1. Align Redis-style operations in `Cache.Sandbox`.
- Match return values and error tuples for hash and JSON operations.
- Ensure scan/hash_scan results match Redis adapter expectations.

2. Make ETS/DETS APIs sandbox-aware.
- Update `Cache.ETS` and `Cache.DETS` macros to delegate to `Cache.Sandbox` when `sandbox?` is true.
- Implement missing sandbox equivalents for ETS/DETS conversion and file APIs required by tests.
- Ensure `info/0` returns the `:name` field for ETS.

3. Implement ConCache API parity.
- Add `get_or_store/3` and `dirty_get_or_store/2` in `Cache.Sandbox` with matching semantics.

4. Add sandbox parity tests.
- Mirror adapter-specific tests (Redis hash/JSON, ETS, DETS, ConCache) using sandbox-enabled caches.
- Keep expectations identical to adapter tests.

## Verification
- Run adapter tests against sandbox-enabled modules.
- Confirm no warnings and that new parity tests pass.
306 changes: 237 additions & 69 deletions lib/cache/dets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,162 @@ defmodule Cache.DETS do

defmacro __using__(_opts) do
quote do
@doc """
Returns a list of the names of all open DETS tables on this node.
"""
def all do
:dets.all()
end

@doc """
Returns a list of objects stored in the table (binary chunk format).
"""
def bchunk(continuation) do
:dets.bchunk(@cache_name, continuation)
end

@doc """
Closes the DETS table.
"""
def close do
:dets.close(@cache_name)
end

@doc """
Deletes all objects in the DETS table.
"""
def delete_all_objects do
:dets.delete_all_objects(@cache_name)
end

@doc """
Deletes the exact object from the DETS table.
"""
def delete_object(object) do
:dets.delete_object(@cache_name, object)
end

@doc """
Returns the first key in the table.
"""
def first do
:dets.first(@cache_name)
end

@doc """
Folds over all objects in the table.
"""
def foldl(function, acc) do
:dets.foldl(function, acc, @cache_name)
end

@doc """
Folds over all objects in the table (same as foldl for DETS).
"""
def foldr(function, acc) do
:dets.foldr(function, acc, @cache_name)
end

@doc """
Get information about the DETS table.
"""
def info do
:dets.info(@cache_name)
end

@doc """
Get specific information about the DETS table.
"""
def info(item) do
:dets.info(@cache_name, item)
end

@doc """
Replaces the existing objects of the table with objects created by calling the input function.
"""
def init_table(init_fun) do
:dets.init_table(@cache_name, init_fun)
end

@doc """
Replaces the existing objects of the table with objects created by calling the input function with options.
"""
def init_table(init_fun, options) do
:dets.init_table(@cache_name, init_fun, options)
end

@doc """
Insert raw data into the DETS table using the underlying :dets.insert/2 function.
"""
def insert_raw(data) do
:dets.insert(@cache_name, data)
end

@doc """
Same as insert/2 except returns false if any object with the same key already exists.
"""
def insert_new(data) do
:dets.insert_new(@cache_name, data)
end

@doc """
Returns true if it would be possible to initialize the table with bchunk data.
"""
# credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames
def is_compatible_bchunk_format(bchunk_format) do
:dets.is_compatible_bchunk_format(@cache_name, bchunk_format)
end

@doc """
Returns true if the file is a DETS table.
"""
# credo:disable-for-next-line Credo.Check.Readability.PredicateFunctionNames
def is_dets_file(filename) do
:dets.is_dets_file(filename)
end

@doc """
Returns a list of all objects with the given key.
"""
def lookup(key) do
:dets.lookup(@cache_name, key)
end

@doc """
Continues a match started with match/2.
"""
def match(continuation) when not is_tuple(continuation) do
:dets.match(continuation)
end

@doc """
Matches the objects in the table against the pattern.
"""
def match(pattern) do
:dets.match(@cache_name, pattern)
end

@doc """
Matches the objects in the table against the pattern with a limit.
"""
def match(pattern, limit) do
:dets.match(@cache_name, pattern, limit)
end

@doc """
Deletes all objects that match the pattern from the table.
"""
def match_delete(pattern) do
:dets.match_delete(@cache_name, pattern)
end

@doc """
Continues a match_object started with match_object/2.
"""
def match_object(continuation) when not is_tuple(continuation) do
:dets.match_object(continuation)
end

@doc """
Match objects in the DETS table that match the given pattern.

Expand Down Expand Up @@ -66,6 +222,55 @@ defmodule Cache.DETS do
:dets.member(@cache_name, key)
end

@doc """
Returns the next key following the given key.
"""
def next(key) do
:dets.next(@cache_name, key)
end

@doc """
Opens an existing DETS table file.
"""
def open_file(filename) do
:dets.open_file(filename)
end

@doc """
Opens a DETS table with the given name and arguments.
"""
def open_file(name, args) do
:dets.open_file(name, args)
end

@doc """
Returns the table name given the pid of a process that handles requests to a table.
"""
def pid2name(pid) do
:dets.pid2name(pid)
end

@doc """
Restores an opaque continuation that has passed through external term format.
"""
def repair_continuation(continuation, match_spec) do
:dets.repair_continuation(continuation, match_spec)
end

@doc """
Fixes the table for safe traversal.
"""
def safe_fixtable(fix) do
:dets.safe_fixtable(@cache_name, fix)
end

@doc """
Continues a select started with select/2.
"""
def select(continuation) when not is_list(continuation) do
:dets.select(continuation)
end

@doc """
Select objects from the DETS table using a match specification.

Expand All @@ -91,103 +296,66 @@ defmodule Cache.DETS do
end

@doc """
Get information about the DETS table.

## Examples

iex> #{inspect(__MODULE__)}.info()
[...]
Delete objects from the DETS table using a match specification.
"""
def info do
:dets.info(@cache_name)
def select_delete(match_spec) do
:dets.select_delete(@cache_name, match_spec)
end

@doc """
Get specific information about the DETS table.

## Examples

iex> #{inspect(__MODULE__)}.info(:size)
42
Returns the list of objects associated with slot I.
"""
def info(item) do
:dets.info(@cache_name, item)
def slot(i) do
:dets.slot(@cache_name, i)
end

@doc """
Delete objects from the DETS table using a match specification.

## Examples

iex> #{inspect(__MODULE__)}.select_delete([{{:key, :_}, [], [true]}])
42
Ensures that all updates made to the table are written to disk.
"""
def select_delete(match_spec) do
:dets.select_delete(@cache_name, match_spec)
def sync do
:dets.sync(@cache_name)
end

@doc """
Delete objects from the DETS table that match the given pattern.
Returns a QLC query handle for the table.
"""
def table do
:dets.table(@cache_name)
end

## Examples
@doc """
Returns a QLC query handle for the table with options.
"""
def table(options) do
:dets.table(@cache_name, options)
end

iex> #{inspect(__MODULE__)}.match_delete({:key, :_})
:ok
@doc """
Applies a function to each object stored in the table.
"""
def match_delete(pattern) do
:dets.match_delete(@cache_name, pattern)
def traverse(fun) do
:dets.traverse(@cache_name, fun)
end

@doc """
Update a counter in the DETS table.

## Examples

iex> #{inspect(__MODULE__)}.update_counter(:counter_key, {2, 1})
43
"""
def update_counter(key, update_op) do
:dets.update_counter(@cache_name, key, update_op)
end

@doc """
Insert raw data into the DETS table using the underlying :dets.insert/2 function.

## Examples

iex> #{inspect(__MODULE__)}.insert_raw({:key, "value"})
:ok
iex> #{inspect(__MODULE__)}.insert_raw([{:key1, "value1"}, {:key2, "value2"}])
:ok
Convert a DETS table to the given ETS table.
"""
def insert_raw(data) do
:dets.insert(@cache_name, data)
def to_ets(ets_table) do
:dets.to_ets(@cache_name, ets_table)
end

if function_exported?(:dets, :to_ets, 1) do
@doc """
Convert a DETS table to an ETS table.

## Examples

iex> #{inspect(__MODULE__)}.to_ets()
:my_ets_table
"""
def to_ets do
:dets.to_ets(@cache_name)
end

@doc """
Convert an ETS table to a DETS table.

## Examples

iex> #{inspect(__MODULE__)}.from_ets(:my_ets_table)
:ok
"""
def from_ets(ets_table) do
:dets.from_ets(@cache_name, ets_table)
end
@doc """
Convert an ETS table to a DETS table.
"""
def from_ets(ets_table) do
:dets.from_ets(@cache_name, ets_table)
end
end
end
Expand Down
Loading
Loading