Skip to content

Commit 59d5721

Browse files
refactor: Rename SanitizationMigrationWrapper to MigrationWrapper
The wrapper is already generic and doesn't depend on sanitization strategies. It manages dual-store migration with caching and lazy migration, making it useful for various migration scenarios: - Sanitization strategy changes - Encryption configuration changes - Compression setting changes - Backend store migrations Changes: - Renamed SanitizationMigrationWrapper to MigrationWrapper - Renamed sanitization_migration directory to migration - Updated all imports and references - Added pytest-asyncio markers to test files - Updated documentation with generic examples - Regenerated sync code Co-authored-by: William Easton <[email protected]>
1 parent baf413f commit 59d5721

File tree

12 files changed

+157
-161
lines changed

12 files changed

+157
-161
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Migration wrapper for gradual store transitions.
2+
3+
This module provides a wrapper that enables gradual migration between two stores
4+
(e.g., stores with different configurations) without breaking access to existing data.
5+
"""
6+
7+
from key_value.aio.wrappers.migration.wrapper import MigrationWrapper
8+
9+
__all__ = ["MigrationWrapper"]
Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
"""Wrapper for migrating between sanitization strategies.
1+
"""Wrapper for migrating between two key-value stores.
22
3-
This wrapper enables gradual migration from one sanitization strategy to another
4-
without breaking access to existing data. It wraps two stores configured with
5-
different sanitization strategies and provides:
3+
This wrapper enables gradual migration from one store configuration to another
4+
without breaking access to existing data. It wraps two stores (current and legacy)
5+
and provides:
66
7-
1. **Collision detection**: Skips migration if both strategies produce the same key
7+
1. **Dual-store read**: Tries current store first, falls back to legacy
88
2. **LRU caching**: Caches lookup results to avoid repeated fallback checks
9-
3. **Lazy migration**: Optionally migrates keys from old to new format on read
9+
3. **Lazy migration**: Optionally migrates keys from legacy to current on read
1010
"""
1111

1212
from collections.abc import Mapping, Sequence
@@ -19,47 +19,52 @@
1919
from key_value.aio.wrappers.base import BaseWrapper
2020

2121

22-
class SanitizationMigrationWrapper(BaseWrapper):
23-
"""Wrapper for migrating between sanitization strategies.
22+
class MigrationWrapper(BaseWrapper):
23+
"""Wrapper for migrating between two key-value stores.
2424
25-
This wrapper manages the transition between two stores using different sanitization
26-
strategies. It tries the current store first, then falls back to the legacy store
27-
if the key is not found. Optionally, it can migrate keys from the legacy store to
28-
the current store on read.
25+
This wrapper manages the transition between two stores (e.g., stores with different
26+
configurations such as sanitization strategies, encryption, compression, etc.).
27+
It tries the current store first, then falls back to the legacy store if the key
28+
is not found. Optionally, it can migrate keys from the legacy store to the current
29+
store on read.
2930
3031
The wrapper includes:
31-
- **Collision detection**: If both strategies produce the same sanitized key, no
32-
fallback is performed (no migration needed)
32+
- **Dual-store read**: Tries current store first, falls back to legacy
3333
- **LRU cache**: Caches which keys are in current vs. legacy store to avoid
3434
repeated lookups
3535
- **Lazy migration**: Optionally copies data from legacy to current store on read
3636
37-
Example:
37+
Common use cases:
38+
- Migrating between sanitization strategies
39+
- Migrating between encryption configurations
40+
- Migrating between compression settings
41+
- Migrating between different backend stores
42+
43+
Example (sanitization strategy migration):
3844
```python
3945
from key_value.aio.stores.memcached import MemcachedStore
4046
from key_value.shared.utils.sanitization_strategy import (
4147
HashLongKeysSanitizationStrategy,
4248
)
43-
from key_value.aio.wrappers.sanitization_migration import (
44-
SanitizationMigrationWrapper,
45-
)
49+
from key_value.aio.wrappers.migration import MigrationWrapper
4650
47-
# Old strategy (no prefix - collision risk!)
48-
old_strategy = HashLongKeysSanitizationStrategy(max_length=240)
51+
# Legacy store with old configuration
4952
legacy_store = MemcachedStore(
5053
host="localhost",
51-
sanitization_strategy=old_strategy,
54+
sanitization_strategy=HashLongKeysSanitizationStrategy(max_length=240),
5255
)
5356
54-
# New strategy (with H_ prefix - collision safe)
55-
new_strategy = HashLongKeysSanitizationStrategy(max_length=240)
57+
# Current store with new configuration
5658
current_store = MemcachedStore(
5759
host="localhost",
58-
sanitization_strategy=new_strategy,
60+
sanitization_strategy=HashLongKeysSanitizationStrategy(
61+
max_length=240,
62+
add_prefix=True,
63+
),
5964
)
6065
6166
# Wrap with migration support
62-
migrating_store = SanitizationMigrationWrapper(
67+
migrating_store = MigrationWrapper(
6368
current_store=current_store,
6469
legacy_store=legacy_store,
6570
migrate_on_read=True, # Copy old data to new location
@@ -68,8 +73,8 @@ class SanitizationMigrationWrapper(BaseWrapper):
6873
```
6974
7075
Args:
71-
current_store: Store using the new sanitization strategy.
72-
legacy_store: Store using the old sanitization strategy.
76+
current_store: The target store (new configuration).
77+
legacy_store: The source store (old configuration).
7378
migrate_on_read: If True, copy keys from legacy to current on read.
7479
delete_after_migration: If True, delete from legacy after migration.
7580
cache_size: Maximum number of keys to cache. Set to 0 to disable caching.
@@ -94,8 +99,8 @@ def __init__(
9499
"""Initialize the migration wrapper.
95100
96101
Args:
97-
current_store: Store using the new sanitization strategy.
98-
legacy_store: Store using the old sanitization strategy.
102+
current_store: The target store (new configuration).
103+
legacy_store: The source store (old configuration).
99104
migrate_on_read: If True, copy keys from legacy to current on read.
100105
delete_after_migration: If True, delete from legacy after migration.
101106
cache_size: Maximum number of keys to cache. Set to 0 to disable.

key-value/key-value-aio/src/key_value/aio/wrappers/sanitization_migration/__init__.py

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
"""Tests for SanitizationMigrationWrapper."""
1+
"""Tests for MigrationWrapper."""
22

33
import pytest
44

55
from key_value.aio.stores.memory import MemoryStore
6-
from key_value.aio.wrappers.sanitization_migration import SanitizationMigrationWrapper
6+
from key_value.aio.wrappers.migration import MigrationWrapper
77

8+
pytestmark = pytest.mark.asyncio
89

9-
class TestSanitizationMigrationWrapper:
10-
"""Tests for SanitizationMigrationWrapper."""
10+
11+
class TestMigrationWrapper:
12+
"""Tests for MigrationWrapper."""
1113

1214
@pytest.fixture
1315
def current_store(self) -> MemoryStore:
@@ -20,37 +22,35 @@ def legacy_store(self) -> MemoryStore:
2022
return MemoryStore()
2123

2224
@pytest.fixture
23-
def wrapper(self, current_store: MemoryStore, legacy_store: MemoryStore) -> SanitizationMigrationWrapper:
25+
def wrapper(self, current_store: MemoryStore, legacy_store: MemoryStore) -> MigrationWrapper:
2426
"""Create a migration wrapper."""
25-
return SanitizationMigrationWrapper(
27+
return MigrationWrapper(
2628
current_store=current_store,
2729
legacy_store=legacy_store,
2830
migrate_on_read=False,
2931
cache_size=100,
3032
)
3133

3234
@pytest.fixture
33-
def migrating_wrapper(self, current_store: MemoryStore, legacy_store: MemoryStore) -> SanitizationMigrationWrapper:
35+
def migrating_wrapper(self, current_store: MemoryStore, legacy_store: MemoryStore) -> MigrationWrapper:
3436
"""Create a migration wrapper with migrate_on_read=True."""
35-
return SanitizationMigrationWrapper(
37+
return MigrationWrapper(
3638
current_store=current_store,
3739
legacy_store=legacy_store,
3840
migrate_on_read=True,
3941
delete_after_migration=False,
4042
cache_size=100,
4143
)
4244

43-
async def test_get_from_current_store(self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore) -> None:
45+
async def test_get_from_current_store(self, wrapper: MigrationWrapper, current_store: MemoryStore) -> None:
4446
"""Test getting a value from the current store."""
4547
await current_store.put(key="test_key", value={"data": "current"}, collection="default")
4648

4749
result = await wrapper.get(key="test_key", collection="default")
4850
assert result is not None
4951
assert result["data"] == "current"
5052

51-
async def test_get_from_legacy_store(
52-
self, wrapper: SanitizationMigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
53-
) -> None:
53+
async def test_get_from_legacy_store(self, wrapper: MigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore) -> None:
5454
"""Test getting a value from the legacy store when not in current."""
5555
await legacy_store.put(key="test_key", value={"data": "legacy"}, collection="default")
5656

@@ -62,13 +62,13 @@ async def test_get_from_legacy_store(
6262
current_result = await current_store.get(key="test_key", collection="default")
6363
assert current_result is None
6464

65-
async def test_get_missing_key(self, wrapper: SanitizationMigrationWrapper) -> None:
65+
async def test_get_missing_key(self, wrapper: MigrationWrapper) -> None:
6666
"""Test getting a missing key returns None."""
6767
result = await wrapper.get(key="missing_key", collection="default")
6868
assert result is None
6969

7070
async def test_migrate_on_read(
71-
self, migrating_wrapper: SanitizationMigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
71+
self, migrating_wrapper: MigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
7272
) -> None:
7373
"""Test that migrate_on_read copies data from legacy to current."""
7474
await legacy_store.put(key="test_key", value={"data": "legacy"}, collection="default", ttl=3600)
@@ -88,7 +88,7 @@ async def test_migrate_on_read(
8888

8989
async def test_migrate_on_read_with_delete(self, legacy_store: MemoryStore, current_store: MemoryStore) -> None:
9090
"""Test that delete_after_migration removes from legacy."""
91-
wrapper = SanitizationMigrationWrapper(
91+
wrapper = MigrationWrapper(
9292
current_store=current_store,
9393
legacy_store=legacy_store,
9494
migrate_on_read=True,
@@ -106,7 +106,7 @@ async def test_migrate_on_read_with_delete(self, legacy_store: MemoryStore, curr
106106
assert legacy_result is None
107107

108108
async def test_cache_avoids_repeated_lookups(
109-
self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore
109+
self, wrapper: MigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore
110110
) -> None:
111111
"""Test that cache avoids repeated lookups."""
112112
await current_store.put(key="test_key", value={"data": "current"}, collection="default")
@@ -125,7 +125,7 @@ async def test_cache_avoids_repeated_lookups(
125125
assert result2 is not None
126126
assert result2["data"] == "current" # Still from current, not legacy
127127

128-
async def test_cache_missing_keys(self, wrapper: SanitizationMigrationWrapper) -> None:
128+
async def test_cache_missing_keys(self, wrapper: MigrationWrapper) -> None:
129129
"""Test that missing keys are cached."""
130130
# First get - should cache as missing
131131
result1 = await wrapper.get(key="missing_key", collection="default")
@@ -139,9 +139,7 @@ async def test_cache_missing_keys(self, wrapper: SanitizationMigrationWrapper) -
139139
result2 = await wrapper.get(key="missing_key", collection="default")
140140
assert result2 is None
141141

142-
async def test_put_updates_cache(
143-
self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore
144-
) -> None:
142+
async def test_put_updates_cache(self, wrapper: MigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
145143
"""Test that put updates the cache."""
146144
# Put initially in legacy
147145
await legacy_store.put(key="test_key", value={"data": "legacy"}, collection="default")
@@ -159,9 +157,7 @@ async def test_put_updates_cache(
159157
assert result is not None
160158
assert result["data"] == "new"
161159

162-
async def test_delete_from_both_stores(
163-
self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore
164-
) -> None:
160+
async def test_delete_from_both_stores(self, wrapper: MigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
165161
"""Test that delete removes from both stores."""
166162
await current_store.put(key="key1", value={"data": "current"}, collection="default")
167163
await legacy_store.put(key="key2", value={"data": "legacy"}, collection="default")
@@ -177,7 +173,7 @@ async def test_delete_from_both_stores(
177173
assert await current_store.get(key="key1", collection="default") is None
178174
assert await legacy_store.get(key="key2", collection="default") is None
179175

180-
async def test_get_many(self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
176+
async def test_get_many(self, wrapper: MigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
181177
"""Test get_many with keys in different stores."""
182178
await current_store.put(key="key1", value={"data": "current1"}, collection="default")
183179
await legacy_store.put(key="key2", value={"data": "legacy2"}, collection="default")
@@ -192,7 +188,7 @@ async def test_get_many(self, wrapper: SanitizationMigrationWrapper, current_sto
192188
assert results[2] is None
193189

194190
async def test_get_many_with_migration(
195-
self, migrating_wrapper: SanitizationMigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
191+
self, migrating_wrapper: MigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
196192
) -> None:
197193
"""Test get_many migrates keys from legacy to current."""
198194
await legacy_store.put(key="key1", value={"data": "legacy1"}, collection="default")
@@ -210,7 +206,7 @@ async def test_get_many_with_migration(
210206
assert current_result1 is not None
211207
assert current_result2 is not None
212208

213-
async def test_ttl_from_current(self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore) -> None:
209+
async def test_ttl_from_current(self, wrapper: MigrationWrapper, current_store: MemoryStore) -> None:
214210
"""Test ttl from current store."""
215211
await current_store.put(key="test_key", value={"data": "current"}, collection="default", ttl=3600)
216212

@@ -220,9 +216,7 @@ async def test_ttl_from_current(self, wrapper: SanitizationMigrationWrapper, cur
220216
assert ttl is not None
221217
assert ttl > 0
222218

223-
async def test_ttl_from_legacy(
224-
self, wrapper: SanitizationMigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
225-
) -> None:
219+
async def test_ttl_from_legacy(self, wrapper: MigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore) -> None:
226220
"""Test ttl from legacy store."""
227221
await legacy_store.put(key="test_key", value={"data": "legacy"}, collection="default", ttl=3600)
228222

@@ -236,7 +230,7 @@ async def test_ttl_from_legacy(
236230
assert current_result is None
237231

238232
async def test_ttl_with_migration(
239-
self, migrating_wrapper: SanitizationMigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
233+
self, migrating_wrapper: MigrationWrapper, legacy_store: MemoryStore, current_store: MemoryStore
240234
) -> None:
241235
"""Test ttl migrates from legacy to current."""
242236
await legacy_store.put(key="test_key", value={"data": "legacy"}, collection="default", ttl=3600)
@@ -250,7 +244,7 @@ async def test_ttl_with_migration(
250244
assert current_value is not None
251245
assert current_ttl is not None
252246

253-
async def test_put_many(self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore) -> None:
247+
async def test_put_many(self, wrapper: MigrationWrapper, current_store: MemoryStore) -> None:
254248
"""Test put_many writes to current store."""
255249
await wrapper.put_many(
256250
keys=["key1", "key2"],
@@ -264,7 +258,7 @@ async def test_put_many(self, wrapper: SanitizationMigrationWrapper, current_sto
264258
assert result1 is not None
265259
assert result2 is not None
266260

267-
async def test_delete_many(self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
261+
async def test_delete_many(self, wrapper: MigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
268262
"""Test delete_many removes from both stores."""
269263
await current_store.put(key="key1", value={"data": "current"}, collection="default")
270264
await legacy_store.put(key="key2", value={"data": "legacy"}, collection="default")
@@ -278,7 +272,7 @@ async def test_delete_many(self, wrapper: SanitizationMigrationWrapper, current_
278272

279273
async def test_cache_disabled(self, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
280274
"""Test wrapper with caching disabled."""
281-
wrapper = SanitizationMigrationWrapper(
275+
wrapper = MigrationWrapper(
282276
current_store=current_store,
283277
legacy_store=legacy_store,
284278
cache_size=0, # Disable cache
@@ -292,7 +286,7 @@ async def test_cache_disabled(self, current_store: MemoryStore, legacy_store: Me
292286
# Cache should be disabled
293287
assert not wrapper._cache_enabled # pyright: ignore[reportPrivateUsage]
294288

295-
async def test_ttl_many(self, wrapper: SanitizationMigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
289+
async def test_ttl_many(self, wrapper: MigrationWrapper, current_store: MemoryStore, legacy_store: MemoryStore) -> None:
296290
"""Test ttl_many with keys in different stores."""
297291
await current_store.put(key="key1", value={"data": "current1"}, collection="default", ttl=3600)
298292
await legacy_store.put(key="key2", value={"data": "legacy2"}, collection="default", ttl=7200)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# WARNING: this file is auto-generated by 'build_sync_library.py'
2+
# from the original file '__init__.py'
3+
# DO NOT CHANGE! Change the original file instead.
4+
"""Migration wrapper for gradual store transitions.
5+
6+
This module provides a wrapper that enables gradual migration between two stores
7+
(e.g., stores with different configurations) without breaking access to existing data.
8+
"""
9+
10+
from key_value.sync.code_gen.wrappers.migration.wrapper import MigrationWrapper
11+
12+
__all__ = ["MigrationWrapper"]

0 commit comments

Comments
 (0)