Skip to main content

storage/sqlite: fix a bug when storing UTF-8 values

ID
33d832a
date
2026-11-06 21:57:32+00:00
author
Alex Chan <alexc@tailscale.com>
parent
0246dc9
message
storage/sqlite: fix a bug when storing UTF-8 values

Encoding the JSON as ASCII means only ASCII values can be stored, which
isn't the intention.
changed files
2 files, 49 additions, 30 deletions

Changed files

src/funkycache/storage.py (6794 → 6793)

diff --git a/src/funkycache/storage.py b/src/funkycache/storage.py
index a9f009c..abfad40 100644
--- a/src/funkycache/storage.py
+++ b/src/funkycache/storage.py
@@ -112,7 +112,7 @@ class TypedCacheStorage[V](CacheStorage[V]):
         Throws an exception if you overwrite an existing key with
         a different value.
         """
-        json_str = self.adapter.dump_json(value).decode("ascii")
+        json_str = self.adapter.dump_json(value).decode("utf8")
         self._set_str(key, json_str)
 
     @abstractmethod

tests/test_storage.py (4073 → 4736)

diff --git a/tests/test_storage.py b/tests/test_storage.py
index c448a19..0789c9e 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -3,7 +3,8 @@ Tests for backend storage implementations.
 """
 
 from abc import ABC, abstractmethod
-from collections.abc import Iterator, Mapping
+from collections.abc import Generator, Mapping
+from contextlib import contextmanager
 from pathlib import Path
 import sqlite3
 from typing import Any
@@ -28,10 +29,12 @@ class CacheStorageTestSuite(ABC):
     """
 
     @abstractmethod
-    @pytest.fixture
-    def storage(self) -> Iterator[CacheStorage[int]]:
+    @contextmanager
+    def create_storage[V](
+        self, tmp_path: Path, data_type: Any
+    ) -> Generator[CacheStorage[V]]:
         """
-        Return an empty cache storage backend that holds integer values.
+        Return an empty cache storage backend.
         """
 
     @pytest.mark.parametrize(
@@ -45,31 +48,41 @@ class CacheStorageTestSuite(ABC):
             pytest.param({"n": Path("10.txt")}, id="path"),
         ],
     )
-    def test_cache(self, storage: CacheStorage[int], key: Mapping[str, Any]) -> None:
+    def test_cache(self, key: Mapping[str, Any], tmp_path: Path) -> None:
         """
         Test the basic properties of a cache.
         """
-        fib10 = 55
-        fib20 = 6765
+        with self.create_storage(tmp_path, data_type=int) as storage:
+            fib10 = 55
+            fib20 = 6765
 
-        # Test empty storage
-        with pytest.raises(KeyError):
-            storage.get(key)
+            # Test empty storage
+            with pytest.raises(KeyError):
+                storage.get(key)
 
-        # Store and retrieve a value
-        storage.set(key, value=fib10)
-        assert storage.get(key) == fib10
+            # Store and retrieve a value
+            storage.set(key, value=fib10)
+            assert storage.get(key) == fib10
 
-        # Overwriting an existing value is an error.
-        with pytest.raises(CannotOverwriteValueException) as exc:
-            storage.set(key, value=fib20)
-        assert repr(key) in exc.value.args[0]
+            # Overwriting an existing value is an error.
+            with pytest.raises(CannotOverwriteValueException) as exc:
+                storage.set(key, value=fib20)
+            assert repr(key) in exc.value.args[0]
 
-        assert storage.get(key) == fib10
+            assert storage.get(key) == fib10
 
-        # Writing a key with the same value as the already-stored
-        # value is allowed.
-        storage.set(key, value=fib10)
+            # Writing a key with the same value as the already-stored
+            # value is allowed.
+            storage.set(key, value=fib10)
+
+    def test_can_store_non_ascii_values(self, tmp_path: Path) -> None:
+        """
+        The storage can hold values which don't encode as ASCII.
+        """
+        with self.create_storage(tmp_path, data_type=str) as storage:
+            key = {"type": "place"}
+            storage.set(key, value="café")
+            assert storage.get(key) == "café"
 
 
 class TestMemoryCacheStorage(CacheStorageTestSuite):
@@ -77,12 +90,14 @@ class TestMemoryCacheStorage(CacheStorageTestSuite):
     Tests for MemoryCacheStorage.
     """
 
-    @pytest.fixture
-    def storage(self) -> Iterator[CacheStorage[int]]:
+    @contextmanager
+    def create_storage[V](
+        self, tmp_path: Path, data_type: Any
+    ) -> Generator[CacheStorage[V]]:
         """
-        Return an empty in-memory cache that returns integer values.
+        Return an empty cache storage backend.
         """
-        yield MemoryCacheStorage[int]()
+        yield MemoryCacheStorage[V]()
 
 
 class TestSQLiteCacheStorage(CacheStorageTestSuite):
@@ -90,12 +105,16 @@ class TestSQLiteCacheStorage(CacheStorageTestSuite):
     Tests for SQLiteCacheStorage.
     """
 
-    @pytest.fixture
-    def storage(self) -> Iterator[CacheStorage[int]]:
+    @contextmanager
+    def create_storage[V](
+        self, tmp_path: Path, data_type: Any
+    ) -> Generator[CacheStorage[V]]:
         """
-        Return an empty in-memory cache that returns integer values.
+        Return an empty cache storage backend.
         """
-        yield SQLiteCacheStorage[int](":memory:", data_type=int)
+        storage = SQLiteCacheStorage(":memory:", data_type=data_type)
+        yield storage
+        storage.close()
 
     def test_operations_persist(self, tmp_path: Path) -> None:
         """