Add delete and remove operations for entry management
This commit is contained in:
34
README.md
34
README.md
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user