diff --git a/src/grung/db.py b/src/grung/db.py index 1bad4fc..1ab7cd3 100644 --- a/src/grung/db.py +++ b/src/grung/db.py @@ -1,5 +1,6 @@ import inspect import re +from collections.abc import Iterable from functools import reduce from operator import ior from pathlib import Path @@ -9,7 +10,7 @@ from tinydb import Query, TinyDB, table from tinydb.storages import MemoryStorage from tinydb.table import Document -from grung.exceptions import UniqueConstraintError +from grung.exceptions import CircularReferenceError, UniqueConstraintError from grung.types import Record @@ -66,8 +67,15 @@ class RecordTable(table.Table): super().remove(doc_ids=[document.doc_id]) def _check_constraints(self, document) -> bool: + self._check_for_recursion(document) self._check_unique(document) + def _check_for_recursion(self, document) -> bool: + ref = document.reference + for field in document._metadata.fields.values(): + if isinstance(field.default, Iterable) and ref in document[field.name]: + raise CircularReferenceError(ref, field) + def _check_unique(self, document) -> bool: matches = [] queries = reduce( diff --git a/src/grung/exceptions.py b/src/grung/exceptions.py index 711e32c..5b474d8 100644 --- a/src/grung/exceptions.py +++ b/src/grung/exceptions.py @@ -9,7 +9,7 @@ class UniqueConstraintError(Exception): f" * Record: {dict(document)}\n" f" * Query: {query}\n" f" * Error: Unique constraint failure\n" - " * The record matches the following existing records:\n\n" + "\n".join(str(c) for c in collisions) + " * The record matches the following existing records:\n\n" + "\n".join(str(c) for c in collisions) ) @@ -25,3 +25,18 @@ class PointerReferenceError(Exception): f" * Error: Invalid Pointer\n" " * This collection member does not refer an existing record. Do you need to save it first?" ) + + +class CircularReferenceError(Exception): + """ + Thrown when a record contains a reference to itself. + """ + + def __init__(self, reference, field): + super().__init__( + "\n" + f" * Reference: {reference}\n" + f" * Field: {field.name}\n" + f" * Error: Circular Reference\n" + f" * This record contains a reference to itself. This will lead to infinite recursion." + ) diff --git a/src/grung/types.py b/src/grung/types.py index 692a2f6..a6614a7 100644 --- a/src/grung/types.py +++ b/src/grung/types.py @@ -257,8 +257,6 @@ class Pointer(Field): raise PointerReferenceError("Value {value} does not look like a reference!") return value if value: - if not value.doc_id: - raise PointerReferenceError(value) return f"{value._metadata.table}::{value._metadata.primary_key}::{value[value._metadata.primary_key]}" return None diff --git a/test/test_db.py b/test/test_db.py index cad3716..edff94b 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -10,7 +10,7 @@ from tinydb.storages import MemoryStorage from grung import examples from grung.db import GrungDB -from grung.exceptions import PointerReferenceError, UniqueConstraintError +from grung.exceptions import CircularReferenceError, UniqueConstraintError @pytest.fixture @@ -49,13 +49,7 @@ def test_crud(db): def test_pointers(db): - user = examples.User(name="john", email="john@foo") - players = examples.Group(name="players", members=[user]) - - with pytest.raises(PointerReferenceError): - players = db.save(players) - - user = db.save(user) + user = db.save(examples.User(name="john", email="john@foo")) players = db.save(examples.Group(name="players", members=[user])) user = db.table("User").get(doc_id=user.doc_id) @@ -86,6 +80,11 @@ def test_subgroups(db): kirk = db.table("User").get(doc_id=kirk.doc_id) assert kirk.reference in unique_users + # recursion! + with pytest.raises(CircularReferenceError): + tos.members = [tos] + db.save(tos) + def test_unique(db): user1 = examples.User(name="john", email="john@foo")