Add delete and remove operations for entry management
This commit is contained in:
32
README.md
32
README.md
@@ -43,6 +43,9 @@ engine.init("tenant_1")
|
|||||||
engine.save("tenant_1", "user_1", "config", {"theme": "dark", "lang": "en"})
|
engine.save("tenant_1", "user_1", "config", {"theme": "dark", "lang": "en"})
|
||||||
data = engine.load("tenant_1", "config")
|
data = engine.load("tenant_1", "config")
|
||||||
print(data) # {"theme": "dark", "lang": "en"}
|
print(data) # {"theme": "dark", "lang": "en"}
|
||||||
|
|
||||||
|
# Delete an entry
|
||||||
|
engine.delete("tenant_1", "user_1", "config")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
@@ -135,6 +138,32 @@ if engine.exists("tenant_1", "config"):
|
|||||||
print("Entry exists")
|
print("Entry exists")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Deletion Operations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Delete an entire entry (removes from head, keeps history)
|
||||||
|
deleted = engine.delete("tenant_1", "user_1", "config")
|
||||||
|
# Returns True if entry existed, False otherwise
|
||||||
|
|
||||||
|
# Historical snapshots remain accessible by digest
|
||||||
|
old_config = engine.load("tenant_1", "config", digest=history[0])
|
||||||
|
|
||||||
|
# Remove a specific key from an entry (Pattern 2)
|
||||||
|
engine.put("tenant_1", "user_1", "users", "alice", {"name": "Alice", "role": "admin"})
|
||||||
|
engine.put("tenant_1", "user_1", "users", "bob", {"name": "Bob", "role": "user"})
|
||||||
|
|
||||||
|
removed = engine.remove("tenant_1", "user_1", "users", "alice")
|
||||||
|
# Returns True if key existed, False otherwise
|
||||||
|
|
||||||
|
# Entry still exists, only "alice" was removed
|
||||||
|
all_users = engine.get("tenant_1", "users") # Returns only bob
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key differences:**
|
||||||
|
- `delete()`: Removes entire entry from `head` file (works with both Pattern 1 and Pattern 2)
|
||||||
|
- `remove()`: Removes a specific key from an entry, creates new snapshot (Pattern 2 only)
|
||||||
|
- Both operations preserve historical snapshots in `objects/` directory
|
||||||
|
|
||||||
## Custom Serialization
|
## Custom Serialization
|
||||||
|
|
||||||
MyDbEngine supports three approaches for custom serialization:
|
MyDbEngine supports three approaches for custom serialization:
|
||||||
@@ -271,6 +300,8 @@ engine.save("tenant_1", "user_1", "my_data", obj)
|
|||||||
| `put(tenant_id, user_id, entry, key, value) -> bool` | Add/update single record |
|
| `put(tenant_id, user_id, entry, key, value) -> bool` | Add/update single record |
|
||||||
| `put_many(tenant_id, user_id, entry, items) -> bool` | Add/update multiple records |
|
| `put_many(tenant_id, user_id, entry, items) -> bool` | Add/update multiple records |
|
||||||
| `get(tenant_id, entry, key=None, digest=None) -> object` | Get record(s) |
|
| `get(tenant_id, entry, key=None, digest=None) -> object` | Get record(s) |
|
||||||
|
| `delete(tenant_id, user_id, entry) -> bool` | Delete entire entry from head (keeps history) |
|
||||||
|
| `remove(tenant_id, user_id, entry, key) -> bool` | Remove specific key from entry (Pattern 2) |
|
||||||
| `exists(tenant_id, entry) -> bool` | Check if entry exists |
|
| `exists(tenant_id, entry) -> bool` | Check if entry exists |
|
||||||
|
|
||||||
### History
|
### History
|
||||||
@@ -351,3 +382,4 @@ See LICENSE file for details.
|
|||||||
* 0.1.0 - Initial release
|
* 0.1.0 - Initial release
|
||||||
* 0.2.0 - Added custom reference handlers
|
* 0.2.0 - Added custom reference handlers
|
||||||
* 0.2.1 - A handler can only be registered once
|
* 0.2.1 - A handler can only be registered once
|
||||||
|
* 0.3.0 - Added delete() and remove()
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mydbengine"
|
name = "mydbengine"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
description = "A lightweight, git-inspired database engine that maintains complete history of all modifications"
|
description = "A lightweight, git-inspired database engine that maintains complete history of all modifications"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
|
|||||||
@@ -197,6 +197,28 @@ class DbEngine:
|
|||||||
|
|
||||||
return self._deserialize(as_dict)
|
return self._deserialize(as_dict)
|
||||||
|
|
||||||
|
def delete(self, tenant_id: str, user_id: str, entry: str):
|
||||||
|
"""
|
||||||
|
Delete a whole entry
|
||||||
|
:param tenant_id:
|
||||||
|
:param user_id:
|
||||||
|
:param entry:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
logger.info(f"Delete {tenant_id=}, {entry=}")
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
raise DbException("tenant_id is None")
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
raise DbException("user_id is None")
|
||||||
|
|
||||||
|
if not entry:
|
||||||
|
raise DbException("entry is None")
|
||||||
|
|
||||||
|
return self._update_head(tenant_id, entry, None)
|
||||||
|
|
||||||
def put(self, tenant_id: str, user_id, entry, key: str, value: object):
|
def put(self, tenant_id: str, user_id, entry, key: str, value: object):
|
||||||
"""
|
"""
|
||||||
Save a specific record.
|
Save a specific record.
|
||||||
@@ -272,6 +294,31 @@ class DbEngine:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def remove(self, tenant_id: str, user_id, entry: str, key: str):
|
||||||
|
"""
|
||||||
|
Remove a specific record
|
||||||
|
:param tenant_id:
|
||||||
|
:param user_id:
|
||||||
|
:param entry:
|
||||||
|
:param key:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
logger.info(f"Removing {tenant_id=}, {entry=}, {key=}")
|
||||||
|
try:
|
||||||
|
entry_content = self.load(tenant_id, entry)
|
||||||
|
except DbException:
|
||||||
|
entry_content = {}
|
||||||
|
|
||||||
|
# Do not update the digest if the record is not present
|
||||||
|
if key not in entry_content:
|
||||||
|
return False
|
||||||
|
|
||||||
|
del entry_content[key]
|
||||||
|
|
||||||
|
self.save(tenant_id, user_id, entry, entry_content)
|
||||||
|
return True
|
||||||
|
|
||||||
def exists(self, tenant_id, entry: str):
|
def exists(self, tenant_id, entry: str):
|
||||||
"""
|
"""
|
||||||
Tells if an entry exist
|
Tells if an entry exist
|
||||||
@@ -375,12 +422,20 @@ class DbEngine:
|
|||||||
head = {}
|
head = {}
|
||||||
|
|
||||||
# update
|
# update
|
||||||
head[entry] = digest
|
if digest is not None:
|
||||||
|
head[entry] = digest
|
||||||
|
else:
|
||||||
|
if entry not in head:
|
||||||
|
return False # no need to update
|
||||||
|
else:
|
||||||
|
del head[entry]
|
||||||
|
|
||||||
# and save
|
# and save
|
||||||
with open(head_path, 'w') as file:
|
with open(head_path, 'w') as file:
|
||||||
json.dump(head, file)
|
json.dump(head, file)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _get_user_root(self, tenant_id):
|
def _get_user_root(self, tenant_id):
|
||||||
return os.path.join(self.root, tenant_id)
|
return os.path.join(self.root, tenant_id)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
@@ -73,6 +74,15 @@ def dummy_obj_with_ref():
|
|||||||
return DummyObjWithRef(1, "a", data)
|
return DummyObjWithRef(1, "a", data)
|
||||||
|
|
||||||
|
|
||||||
|
def load_head(engine, tenant_id):
|
||||||
|
head_path = os.path.join(engine._get_user_root(tenant_id), engine.HeadFile)
|
||||||
|
try:
|
||||||
|
with open(head_path, 'r') as file:
|
||||||
|
return json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_test_init():
|
def test_i_can_test_init():
|
||||||
if os.path.exists(DB_ENGINE_ROOT):
|
if os.path.exists(DB_ENGINE_ROOT):
|
||||||
shutil.rmtree(DB_ENGINE_ROOT)
|
shutil.rmtree(DB_ENGINE_ROOT)
|
||||||
@@ -271,3 +281,59 @@ def test_i_can_retrieve_history_using_save(engine):
|
|||||||
assert v2["key1"] == DummyObj(1, "a", False)
|
assert v2["key1"] == DummyObj(1, "a", False)
|
||||||
|
|
||||||
assert v2[TAG_PARENT] == [None]
|
assert v2[TAG_PARENT] == [None]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_delete_entries(engine):
|
||||||
|
digest1 = engine.save(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry", {"key1": DummyObj(1, "a", False)})
|
||||||
|
digest2 = engine.save(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry2", {"key1": DummyObj(2, "b", True)})
|
||||||
|
res = engine.delete(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry")
|
||||||
|
|
||||||
|
assert res is True
|
||||||
|
|
||||||
|
# entry is no longer in the head
|
||||||
|
head = load_head(engine, FAKE_TENANT_ID)
|
||||||
|
assert "MyEntry" not in head
|
||||||
|
assert "MyEntry2" in head
|
||||||
|
|
||||||
|
# digests are not removed from the db
|
||||||
|
assert engine.load(FAKE_TENANT_ID, "MyEntry2", digest1) is not None
|
||||||
|
assert engine.load(FAKE_TENANT_ID, "MyEntry2", digest2) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_delete_an_entry_that_does_not_exist(engine):
|
||||||
|
# no exception should be raised
|
||||||
|
res = engine.delete(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry")
|
||||||
|
assert res is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_remove_an_key_from_an_entry(engine):
|
||||||
|
engine.save(FAKE_TENANT_ID,
|
||||||
|
FAKE_USER_EMAIL,
|
||||||
|
"MyEntry",
|
||||||
|
{
|
||||||
|
"key1": DummyObj(1, "a", False),
|
||||||
|
"key2": DummyObj(2, "b", True)
|
||||||
|
})
|
||||||
|
|
||||||
|
res = engine.remove(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry", "key1")
|
||||||
|
assert res is True
|
||||||
|
|
||||||
|
snapshot = engine.load(FAKE_TENANT_ID, "MyEntry")
|
||||||
|
assert "key1" not in snapshot
|
||||||
|
assert "key2" in snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_remove_a_key_that_does_not_exist(engine):
|
||||||
|
# no exception should be raised
|
||||||
|
res = engine.remove(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry", "key1")
|
||||||
|
assert res is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_removing_the_last_key_from_an_entry_not_not_delete_the_entry(engine):
|
||||||
|
engine.save(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry", {"key1": DummyObj(1, "a", False)})
|
||||||
|
res = engine.remove(FAKE_TENANT_ID, FAKE_USER_EMAIL, "MyEntry", "key1")
|
||||||
|
assert res is True
|
||||||
|
|
||||||
|
snapshot = engine.load(FAKE_TENANT_ID, "MyEntry")
|
||||||
|
assert "key1" not in snapshot
|
||||||
|
assert snapshot[TAG_PARENT] is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user