Add delete and remove operations for entry management

This commit is contained in:
2026-02-21 21:44:28 +01:00
parent 662d47ac21
commit 4a04bf50c4
4 changed files with 156 additions and 3 deletions

View File

@@ -43,6 +43,9 @@ engine.init("tenant_1")
engine.save("tenant_1", "user_1", "config", {"theme": "dark", "lang": "en"})
data = engine.load("tenant_1", "config")
print(data) # {"theme": "dark", "lang": "en"}
# Delete an entry
engine.delete("tenant_1", "user_1", "config")
```
## Core Concepts
@@ -135,6 +138,32 @@ if engine.exists("tenant_1", "config"):
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
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_many(tenant_id, user_id, entry, items) -> bool` | Add/update multiple records |
| `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 |
### History
@@ -350,4 +381,5 @@ See LICENSE file for details.
## Version History
* 0.1.0 - Initial release
* 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()

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "mydbengine"
version = "0.2.1"
version = "0.3.0"
description = "A lightweight, git-inspired database engine that maintains complete history of all modifications"
readme = "README.md"
requires-python = ">=3.8"

View File

@@ -197,6 +197,28 @@ class DbEngine:
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):
"""
Save a specific record.
@@ -272,6 +294,31 @@ class DbEngine:
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):
"""
Tells if an entry exist
@@ -375,11 +422,19 @@ class DbEngine:
head = {}
# 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
with open(head_path, 'w') as file:
json.dump(head, file)
return True
def _get_user_root(self, tenant_id):
return os.path.join(self.root, tenant_id)

View File

@@ -1,3 +1,4 @@
import json
import os.path
import shutil
@@ -73,6 +74,15 @@ def dummy_obj_with_ref():
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():
if os.path.exists(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[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