Skip to content

Commit 092ba46

Browse files
committed
Add new rename method + support code to move keys around
1 parent 7466a65 commit 092ba46

File tree

12 files changed

+411
-7
lines changed

12 files changed

+411
-7
lines changed

datastore/adapter/directory.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class ObjectDatastore(
158158
FORWARD_CONTAINS = True
159159
FORWARD_GET_ALL = True
160160
FORWARD_PUT_NEW = True
161+
FORWARD_RENAME = True
161162
FORWARD_STAT = True
162163

163164

@@ -225,6 +226,22 @@ async def delete(self, key: datastore.Key) -> None:
225226
await super().directory_remove(dir_key, key, missing_ok=True)
226227

227228

229+
async def rename(self, key1: datastore.Key, key2: datastore.Key, *,
230+
replace: bool = True) -> None:
231+
"""Renames item *key1* to *key2*
232+
233+
DirectoryTreeDatastore removes the previous directory entry and add a new one.
234+
"""
235+
await super().rename(key1, key2, replace=replace)
236+
237+
if key1 != key2:
238+
dir_key1 = key1.parent.instance('directory')
239+
await super().directory_remove(dir_key1, key1, missing_ok=True)
240+
241+
dir_key2 = key2.parent.instance('directory')
242+
await super().directory_add(dir_key2, key2, create=True)
243+
244+
228245
async def query(self, query: datastore.Query) -> datastore.Cursor:
229246
"""Returns objects matching criteria expressed in `query`.
230247
DirectoryTreeDatastore uses directory entries.

datastore/adapter/keytransform.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class _Adapter(typing.Generic[DS, MD, RT, RV]):
5151

5252
FORWARD_CONTAINS = True
5353
FORWARD_GET_ALL = True
54-
FORWARD_PUT_NEW = False # Still not true in all cases
54+
FORWARD_PUT_NEW = False # Only true if we can also transform the returned name back
55+
FORWARD_RENAME = True
5556
FORWARD_STAT = True
5657

5758
key_transform_fn: _support.FunctionProperty[KEY_TRANSFORM_T]
@@ -126,6 +127,13 @@ async def contains(self, key: datastore.Key) -> bool:
126127
return await super().contains(self._transform_key(key)) # type: ignore[misc, no-any-return]
127128

128129

130+
async def rename(self, key1: datastore.Key, key2: datastore.Key, *,
131+
replace: bool = True) -> None:
132+
"""Renames item *keytransform(key1)* to *keytransform(key2)*"""
133+
await super().rename(self._transform_key(key1), # type: ignore[misc]
134+
self._transform_key(key2), replace=replace)
135+
136+
129137
async def stat(self, key: datastore.Key) -> MD:
130138
"""Returns the metadata of the object named by keytransform(key)."""
131139
return await super().stat(self._transform_key(key)) # type: ignore[misc, no-any-return]

datastore/adapter/logging.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class _Adapter(typing.Generic[DS, MD, RT, RV]):
2323
FORWARD_CONTAINS = True
2424
FORWARD_GET_ALL = True
2525
FORWARD_PUT_NEW = True
26+
FORWARD_RENAME = True
2627
FORWARD_STAT = True
2728

2829
logger: logging.Logger
@@ -106,6 +107,16 @@ async def contains(self, key: datastore.Key) -> bool:
106107
return await super().contains(key) # type: ignore[misc, no-any-return]
107108

108109

110+
async def rename(self, key1: datastore.Key, key2: datastore.Key, *,
111+
replace: bool = True) -> None:
112+
"""Renames item *key1* to *key2*
113+
114+
LoggingDatastore logs the change.
115+
"""
116+
self.logger.info('%s: rename %s → %s', self, key1, key2)
117+
await super().rename(key1, key2, replace=replace) # type: ignore[misc]
118+
119+
109120
async def stat(self, key: datastore.Key) -> MD:
110121
"""Returns metadata about things stored at name *key*
111122

datastore/adapter/mount.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@ async def contains(self, key: datastore.Key) -> bool:
112112
return await ds.contains(subkey)
113113

114114

115+
async def rename(self, key1: datastore.Key, key2: datastore.Key, *,
116+
replace: bool = True) -> None:
117+
ds1, _, subkey1 = self._find_mountpoint(key1)
118+
ds2, _, subkey2 = self._find_mountpoint(key2)
119+
if ds1 is None:
120+
raise KeyError(key1)
121+
if ds2 is None:
122+
raise RuntimeError(f"Cannot rename to {key2}: No datastore mounted at that path")
123+
if ds1 is not ds2:
124+
raise RuntimeError(f"Cannot rename {key1} to {key2}: Cross-mount renames are not supported")
125+
await ds1.rename(subkey1, subkey2, replace=replace)
126+
127+
115128
async def stat(self, key: datastore.Key) -> MD:
116129
ds, _, subkey = self._find_mountpoint(key)
117130
if ds is None:

datastore/adapter/tiered.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,30 @@ async def contains(self, key: datastore.Key) -> bool:
206206
return False
207207

208208

209+
async def rename(self, key1: datastore.Key, key2: datastore.Key, *,
210+
replace: bool = True) -> None:
211+
"""Renames item *key1* to *key2*"""
212+
renamed: typing.List[DS] = []
213+
214+
async def do_rename(store: DS) -> None: # type: ignore[return] # mypy bug
215+
await store.rename(key1, key2, replace=replace)
216+
renamed.append(store)
217+
218+
try:
219+
async with trio.open_nursery() as nursery:
220+
for store in self._stores:
221+
nursery.start_soon(do_rename, store)
222+
except BaseException:
223+
# Try to undo our changes
224+
with trio.CancelScope(shield=True):
225+
for store in reversed(renamed):
226+
try:
227+
await store.rename(key2, key1, replace=False)
228+
except BaseException:
229+
pass # Swallow all exceptions
230+
raise
231+
232+
209233
async def stat(self, key: datastore.Key) -> MD:
210234
"""Returns the metadata of the object named by key. Checks each
211235
datastore in order."""

datastore/core/binarystore.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,33 @@ async def get_all(self, key: key_.Key) -> bytes:
256256
return await (await self.get(key)).collect()
257257

258258

259+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
260+
"""Moves the data at name *key1* to *key2*
261+
262+
Arguments
263+
---------
264+
key1
265+
The key to rename, must exist
266+
key2
267+
The new name of the key, if *replace* is ``False`` this must not exist
268+
replace
269+
Should an existing key at name *key2* be replaced
270+
271+
Raises
272+
------
273+
KeyError
274+
There was no data by name *key1* in the datastore
275+
KeyError
276+
There was already some data at name *key2* in the datastore,
277+
but *replace* was not ``True``
278+
RuntimeError
279+
An internal error occurred
280+
NotImplementedError
281+
This datastore does not support this operation
282+
"""
283+
raise NotImplementedError()
284+
285+
259286
async def stat(self, key: key_.Key) -> util.metadata.StreamMetadata:
260287
"""Returns any metadata associated with the data stream named by `key`
261288
or raises `KeyError` otherwise
@@ -481,6 +508,31 @@ async def contains(self, key: key_.Key) -> bool:
481508
return key in self._collection(key)
482509

483510

511+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
512+
"""Moves the data at name *key1* to *key2*
513+
514+
Arguments
515+
---------
516+
key1
517+
The key to rename, must exist
518+
key2
519+
The new name of the key, if *replace* is ``False`` this must not exist
520+
replace
521+
Should an existing key at name *key2* be replaced
522+
"""
523+
if key1 == key2:
524+
return
525+
526+
collection1 = self._collection(key1)
527+
collection2 = self._collection(key2)
528+
if key1 not in collection1:
529+
raise KeyError(key1)
530+
if not replace and key2 in collection2:
531+
raise KeyError(key2)
532+
collection2[key2] = collection1[key1]
533+
del collection1[key1]
534+
535+
484536
async def stat(self, key: key_.Key) -> util.metadata.StreamMetadata:
485537
"""Returns the length of the byte sequence named by `key` if it exists.
486538
@@ -533,6 +585,7 @@ class Adapter(Datastore):
533585
FORWARD_PUT_NEW: bool = False # Not always true
534586
FORWARD_PUT_NEW_D: typing.Optional[bool] = None # Defaults to the value of `FORWARD_PUT_NEW`
535587
FORWARD_PUT_NEW_I: typing.Optional[bool] = None # Defaults to the value of `FORWARD_PUT_NEW`
588+
FORWARD_RENAME: bool = False
536589
FORWARD_STAT: bool = False
537590

538591
child_datastore: Datastore
@@ -704,6 +757,36 @@ async def contains(self, key: key_.Key) -> bool:
704757
return await Datastore.contains(self, key)
705758

706759

760+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
761+
"""Moves the data at name *key1* to *key2*
762+
763+
Arguments
764+
---------
765+
key1
766+
The key to rename, must exist
767+
key2
768+
The new name of the key, if *replace* is ``False`` this must not exist
769+
replace
770+
Should an existing key at name *key2* be replaced
771+
772+
Raises
773+
------
774+
KeyError
775+
Key *key1* did not exist in the child datastore
776+
KeyError
777+
Key *key2* already exists in the child datastore, but *replace* was not ``True``
778+
RuntimeError
779+
An internal error occurred in the child datastore
780+
NotImplementedError
781+
``FORWARD_RENAME`` is not ``True`` or the child datastore does not
782+
support this operation
783+
"""
784+
if self.FORWARD_RENAME:
785+
return await self.child_datastore.rename(key1, key2, replace=replace)
786+
else:
787+
return await Datastore.rename(self, key1, key2, replace=replace) # Will raise
788+
789+
707790
async def stat(self, key: key_.Key) -> util.metadata.StreamMetadata:
708791
"""Returns the metadata of the stream named by `key` if it exists
709792

datastore/core/objectstore.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,33 @@ async def get_all(self, key: key_.Key) -> typing.List[T_co]:
286286
return await (await self.get(key)).collect()
287287

288288

289+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
290+
"""Moves content at name *key1* to *key2*
291+
292+
Arguments
293+
---------
294+
key1
295+
The key to rename, must exist
296+
key2
297+
The new name of the key, if *replace* is ``False`` this must not exist
298+
replace
299+
Should an existing key at name *key2* be replaced
300+
301+
Raises
302+
------
303+
KeyError
304+
There was no content at name *key1* in the datastore
305+
KeyError
306+
There was already some content at name *key2* in the datastore,
307+
but *replace* was not ``True``
308+
RuntimeError
309+
An internal error occurred
310+
NotImplementedError
311+
This datastore does not support this operation
312+
"""
313+
raise NotImplementedError()
314+
315+
289316
async def stat(self, key: key_.Key) -> util.metadata.ChannelMetadata:
290317
"""Returns any metadata associated with the objects named by `key` or
291318
raises `KeyError` otherwise
@@ -517,6 +544,31 @@ async def contains(self, key: key_.Key) -> bool:
517544
return key in self._collection(key)
518545

519546

547+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
548+
"""Moves the objects at name *key1* to *key2*
549+
550+
Arguments
551+
---------
552+
key1
553+
The key to rename, must exist
554+
key2
555+
The new name of the key, if *replace* is ``False`` this must not exist
556+
replace
557+
Should an existing key at name *key2* be replaced
558+
"""
559+
if key1 == key2:
560+
return
561+
562+
collection1 = self._collection(key1)
563+
collection2 = self._collection(key2)
564+
if key1 not in collection1:
565+
raise KeyError(key1)
566+
if not replace and key2 in collection2:
567+
raise KeyError(key2)
568+
collection2[key2] = collection1[key1]
569+
del collection1[key1]
570+
571+
520572
async def stat(self, key: key_.Key) -> util.metadata.ChannelMetadata:
521573
"""Returns the length of the object list named by `key` if it exists.
522574
@@ -574,6 +626,7 @@ class Adapter(Datastore[T_co], typing.Generic[T_co, U_co]):
574626
FORWARD_PUT_NEW: bool = False # Not always true
575627
FORWARD_PUT_NEW_D: typing.Optional[bool] = None # Defaults to the value of `FORWARD_PUT_NEW`
576628
FORWARD_PUT_NEW_I: typing.Optional[bool] = None # Defaults to the value of `FORWARD_PUT_NEW`
629+
FORWARD_RENAME: bool = False
577630
FORWARD_STAT: bool = False
578631

579632
child_datastore: Datastore[U_co]
@@ -771,6 +824,37 @@ async def contains(self, key: key_.Key) -> bool:
771824
return await Datastore.contains(self, key)
772825

773826

827+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
828+
"""Moves the content at name *key1* to *key2*
829+
830+
Arguments
831+
---------
832+
key1
833+
The key to rename, must exist
834+
key2
835+
The new name of the key, if *replace* is ``False`` this must not exist
836+
replace
837+
Should an existing key at name *key2* be replaced
838+
839+
Raises
840+
------
841+
KeyError
842+
There was no content at name *key1* in the child datastore
843+
KeyError
844+
There was already some content at name *key2* in the child datastore,
845+
but *replace* was not ``True``
846+
RuntimeError
847+
An internal error occurred in the child datastore
848+
NotImplementedError
849+
``FORWARD_RENAME`` is not ``True`` or the child datastore does not
850+
support this operation
851+
"""
852+
if self.FORWARD_RENAME:
853+
return await self.child_datastore.rename(key1, key2, replace=replace)
854+
else:
855+
return await Datastore.rename(self, key1, key2, replace=replace) # Will raise
856+
857+
774858
async def stat(self, key: key_.Key) -> util.metadata.ChannelMetadata:
775859
"""Returns the metadata of the object list named by `key` if it exists
776860

datastore/core/serialize.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,32 @@ async def contains(self, key: key_.Key) -> bool:
213213
return await self.child_datastore.contains(key)
214214

215215

216+
async def rename(self, key1: key_.Key, key2: key_.Key, *, replace: bool = True) -> None:
217+
"""Moves the content at name *key1* to *key2*
218+
219+
Arguments
220+
---------
221+
key1
222+
The key to rename, must exist
223+
key2
224+
The new name of the key, if *replace* is ``False`` this must not exist
225+
replace
226+
Should an existing key at name *key2* be replaced
227+
228+
Raises
229+
------
230+
KeyError
231+
Key *key1* does not exist in the child datastore
232+
KeyError
233+
Key *key2* already exists in the child datastore, but *replace* was not ``True``
234+
RuntimeError
235+
An internal error occurred in the child datastore
236+
NotImplementedError
237+
The child datastore does not support this operation
238+
"""
239+
await self.child_datastore.rename(key1, key2, replace=replace)
240+
241+
216242
async def stat(self, key: key_.Key) -> util.metadata.ChannelMetadata:
217243
"""Returns whether an object named by `key` exists
218244

0 commit comments

Comments
 (0)