From adadd1013e0aa035d00f1e26a90def8b176e7af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sat, 7 May 2022 11:04:40 +0300 Subject: [PATCH 1/6] Add support for defining indexes on models Support ensuring indexes on collections. The `Model.ensure_indexes()` function supports any indexing function that ArangoDB Collection supports. --- README.md | 22 +++++++++++++- arangodantic/models.py | 49 +++++++++++++++++++++++++++++++- arangodantic/tests/conftest.py | 10 +++++++ arangodantic/tests/test_model.py | 21 ++++++++++++++ examples/graph_example.py | 14 +++++++++ examples/readme_example.py | 22 +++++++++++++- 6 files changed, 135 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0ff9b1..335229b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,15 @@ class Owner(BaseModel): class Company(DocumentModel): """Dummy company Arangodantic model.""" + class ArangodanticConfig: + indexes = { + "add_hash_index": [ + {"fields": ["company_id"], "unique": True}, + {"fields": ["owner.first_name"]}, + {"fields": ["owner.last_name"]}, + ], + } + company_id: str owner: Owner @@ -67,6 +76,11 @@ class Company(DocumentModel): class Link(EdgeModel): """Dummy Link Arangodantic model.""" + class ArangodanticConfig: + indexes = { + "add_hash_index": [{"fields": ["_from", "_to", "type"], "unique": True}] + } + type: str @@ -90,10 +104,16 @@ async def main(): configure_shylock(await ShylockAioArangoDBBackend.create(db, f"{prefix}shylock")) configure(db, prefix=prefix, key_gen=uuid4, lock=Lock) - # Create collections if they don't yet exist + # Create collections and indexes if they don't yet exist # Only for demo, you likely want to create the collections in advance. await Company.ensure_collection() + await Company.ensure_indexes() await Link.ensure_collection() + await Link.ensure_indexes() + + # Clean up any existing data. + await Company.truncate_collection() + await Link.truncate_collection() # Let's create some example entries owner = Owner(first_name="John", last_name="Doe") diff --git a/arangodantic/models.py b/arangodantic/models.py index 9365472..16c579d 100644 --- a/arangodantic/models.py +++ b/arangodantic/models.py @@ -1,7 +1,7 @@ import textwrap from abc import ABC from functools import lru_cache -from typing import Optional, Type, TypeVar, Union +from typing import Any, Dict, List, Optional, Type, TypeVar, Union import aioarangodb.exceptions import pydantic @@ -43,6 +43,9 @@ class ArangodanticCollectionConfig(pydantic.BaseModel): collection_name: Optional[str] = Field( None, description="Override the name of the collection to use" ) + indexes: Optional[Dict[str, List[Dict[str, Any]]]] = Field( + None, description="Override the collection indexes." + ) class Model(pydantic.BaseModel, ABC): @@ -239,6 +242,50 @@ async def ensure_collection(cls, *args, **kwargs): if not await db.has_collection(name): await db.create_collection(name, *args, **kwargs) + @classmethod + async def ensure_indexes(cls): + """ + Ensure the collection's indexes are created. + + The configuration for the indexes are defined in the ArangodanticConfig class + for a model. The class should define an `indexes` dictionary, which contains + the desired indexing function to be run, and it's index definitions. + Supported index functions are: + - add_hash_index + - add_geo_index + - add_ttl_index + - add_fulltext_index + - add_persistent_index + - add_skiplist_index + + All field definition key-values are passed to the index function as keyword + arguments. + + Example: + class Node(DocumentModel): + class ArangodanticConfig: + indexes = { + "add_hash_index": [ + {"fields": ["field1", "field2"]}, + {"fields: ["field3"], "unique": True}, + {"fields": ["field4"], "sparse": True}, + {"fields": ["field5"], "deduplicate": True}, + ] + } + """ + cls_config: ArangodanticCollectionConfig = getattr( + cls, "ArangodanticConfig", ArangodanticCollectionConfig() + ) + indexes = None + if getattr(cls_config, "indexes", None): + indexes = cls_config.indexes + + if indexes: + collection = cls.get_collection() + for index_function, index_fields in indexes.items(): + for index_definition in index_fields: + await getattr(collection, index_function)(**index_definition) + @classmethod async def delete_collection(cls, ignore_missing: bool = True): """ diff --git a/arangodantic/tests/conftest.py b/arangodantic/tests/conftest.py index 4fa59d7..f014e6c 100644 --- a/arangodantic/tests/conftest.py +++ b/arangodantic/tests/conftest.py @@ -51,6 +51,9 @@ def rand_str(length: int) -> str: class Identity(DocumentModel): """Dummy identity Arangodantic model.""" + class ArangodanticConfig: + indexes = {"add_hash_index": [{"fields": ["name"]}]} + name: str = "" @@ -79,6 +82,11 @@ async def before_save( class Link(EdgeModel): """Dummy Arangodantic edge model.""" + class ArangodanticConfig: + indexes = { + "add_hash_index": [{"fields": ["_from", "_to", "type"], "unique": True}] + } + type: str @@ -125,6 +133,7 @@ class ArangodanticConfig: @pytest.fixture async def identity_collection(configure_db): await Identity.ensure_collection() + await Identity.ensure_indexes() yield await Identity.delete_collection() @@ -153,6 +162,7 @@ async def extended_identity_collection(configure_db): @pytest.fixture async def link_collection(configure_db): await Link.ensure_collection() + await Link.ensure_indexes() yield await Link.delete_collection() diff --git a/arangodantic/tests/test_model.py b/arangodantic/tests/test_model.py index aca2e70..d23dd6e 100644 --- a/arangodantic/tests/test_model.py +++ b/arangodantic/tests/test_model.py @@ -425,3 +425,24 @@ async def test_find_one_with_sort(identity_collection): found = await Identity.find_one(sort=[("name", DESCENDING)]) assert found.name == "Bob" + + +@pytest.mark.asyncio +async def test_identity_indexes(identity_collection): + collection = Identity.get_collection() + for index_definition in await collection.indexes(): + if index_definition["type"] in {"primary", "edge"}: + continue + + if index_definition["type"] == "hash": + assert "name" in index_definition["fields"] + + +@pytest.mark.asyncio +async def test_link_indexes(link_collection): + collection = Link.get_collection() + for index_definition in await collection.indexes(): + if index_definition["type"] in {"primary", "edge"}: + continue + if index_definition["type"] == "hash": + assert ["_from", "_to", "type"] == index_definition["fields"] diff --git a/examples/graph_example.py b/examples/graph_example.py index f98db41..5a4df07 100644 --- a/examples/graph_example.py +++ b/examples/graph_example.py @@ -21,12 +21,18 @@ class Person(DocumentModel): """Documents describing persons.""" + class ArangodanticConfig: + indexes = {"add_hash_index": [{"fields": ["name"]}]} + name: str class Relation(EdgeModel): """Edge documents describing relation between people.""" + class ArangodanticConfig: + indexes = {"add_hash_index": [{"fields": ["kind"]}]} + kind: str @@ -72,6 +78,14 @@ async def main(): # Only for demo, you likely want to create the graph in advance. await RelationGraph.ensure_graph() + # Clean up before running. + await Person.truncate_collection() + await Relation.truncate_collection() + + # We need to ensure the indexes separately + await Person.ensure_indexes() + await Relation.ensure_indexes() + # Let's create some example persons alice = Person(name="Alice") bob = Person(name="Bob") diff --git a/examples/readme_example.py b/examples/readme_example.py index e9f000c..a593c96 100644 --- a/examples/readme_example.py +++ b/examples/readme_example.py @@ -21,6 +21,15 @@ class Owner(BaseModel): class Company(DocumentModel): """Dummy company Arangodantic model.""" + class ArangodanticConfig: + indexes = { + "add_hash_index": [ + {"fields": ["company_id"], "unique": True}, + {"fields": ["owner.first_name"]}, + {"fields": ["owner.last_name"]}, + ], + } + company_id: str owner: Owner @@ -28,6 +37,11 @@ class Company(DocumentModel): class Link(EdgeModel): """Dummy Link Arangodantic model.""" + class ArangodanticConfig: + indexes = { + "add_hash_index": [{"fields": ["_from", "_to", "type"], "unique": True}] + } + type: str @@ -51,10 +65,16 @@ async def main(): configure_shylock(await ShylockAioArangoDBBackend.create(db, f"{prefix}shylock")) configure(db, prefix=prefix, key_gen=uuid4, lock=Lock) - # Create collections if they don't yet exist + # Create collections and indexes if they don't yet exist # Only for demo, you likely want to create the collections in advance. await Company.ensure_collection() + await Company.ensure_indexes() await Link.ensure_collection() + await Link.ensure_indexes() + + # Clean up any existing data. + await Company.truncate_collection() + await Link.truncate_collection() # Let's create some example entries owner = Owner(first_name="John", last_name="Doe") From 778b36af94b7cbb96cbcc5e4913844b9606e7e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Mon, 9 May 2022 15:17:48 +0300 Subject: [PATCH 2/6] Add index classes to manage different index types Instead of using magic methods, set up index classes to handle all the different indexing cases. --- arangodantic/indexes.py | 146 +++++++++++++++++++++++++++++++++ arangodantic/models.py | 42 +++++----- arangodantic/tests/conftest.py | 7 +- examples/graph_example.py | 5 +- examples/readme_example.py | 17 ++-- 5 files changed, 179 insertions(+), 38 deletions(-) create mode 100644 arangodantic/indexes.py diff --git a/arangodantic/indexes.py b/arangodantic/indexes.py new file mode 100644 index 0000000..5d5132f --- /dev/null +++ b/arangodantic/indexes.py @@ -0,0 +1,146 @@ +from typing import List, Optional + +import pydantic +from aioarangodb.collection import StandardCollection +from pydantic import Field + + +class BaseIndex(pydantic.BaseModel): + """ + Base index model. + + Defines fields common to most of the indexes. The index class should be derived + from this base class, and add the create_index() function which actually creates + the index. + """ + + fields: List = Field(..., description="List of fields to index") + unique: Optional[bool] = Field( + None, description="Whether the index is unique or not." + ) + sparse: Optional[bool] = Field( + None, + description="If set to True, documents with None in the field are also indexed," + " otherwise they're skipped", + ) + deduplicate: Optional[bool] = Field( + None, + description="If set to True, inserting duplicate index values from the same " + "document triggers unique constraint errors.", + ) + name: Optional[str] = Field(None, description="Optional name for the index.") + in_background: Optional[bool] = Field( + None, description="Do not hold the collection lock." + ) + + async def add_index(self, collection: StandardCollection) -> dict: + """ + Creates the index on the collection. + + Override this function in the subclass. + + :param collection: The collection to create the index on. + :return: Dictionary with the index information. + """ + raise NotImplementedError() + + +class HashIndex(BaseIndex): + """ + Creates a hash index on the collection. + """ + + async def add_index(self, collection: StandardCollection): + return await collection.add_hash_index( + fields=self.fields, + unique=self.unique, + sparse=self.sparse, + deduplicate=self.deduplicate, + name=self.name, + in_background=self.in_background, + ) + + +class SkiplistIndex(BaseIndex): + """ + Creates a skiplist index on the collection. + """ + + async def add_index(self, collection: StandardCollection) -> dict: + return await collection.add_skiplist_index( + fields=self.fields, + unique=self.unique, + sparse=self.sparse, + deduplicate=self.deduplicate, + name=self.name, + in_background=self.in_background, + ) + + +class GeoIndex(BaseIndex): + """ + Creates a geo-spatial index on the collection. + """ + + ordered: Optional[bool] = Field( + None, description="Whether the order is longitude, then latitude." + ) + + async def add_index(self, collection: StandardCollection) -> dict: + return await collection.add_geo_index( + fields=self.fields, + ordered=self.ordered, + name=self.name, + in_background=self.in_background, + ) + + +class FulltextIndex(BaseIndex): + """ + Creates a fulltext index on the collection. + """ + + min_length: Optional[int] = Field( + None, description="Minimum number of characters to index." + ) + + async def add_index(self, collection: StandardCollection) -> dict: + return await collection.add_fulltext_index( + fields=self.fields, + min_length=self.min_length, + name=self.name, + in_background=self.in_background, + ) + + +class PersistentIndex(BaseIndex): + """ + Creates a persistent index on the collection. + """ + + async def add_index(self, collection: StandardCollection) -> dict: + return await collection.add_persistent_index( + fields=self.fields, + unique=self.unique, + sparse=self.sparse, + name=self.name, + in_background=self.in_background, + ) + + +class TTLIndex(BaseIndex): + """ + Creates a TTL (time-to-live) index. + """ + + expiry_time: int = Field( + ..., description="Time of expiry in seconds after document creation." + ) + + async def add_index(self, collection: StandardCollection) -> dict: + return await collection.add_ttl_index( + fields=self.fields, + expiry_time=self.expiry_time, + name=self.name, + in_background=self.in_background, + ) diff --git a/arangodantic/models.py b/arangodantic/models.py index 16c579d..4b3b904 100644 --- a/arangodantic/models.py +++ b/arangodantic/models.py @@ -1,7 +1,7 @@ import textwrap from abc import ABC from functools import lru_cache -from typing import Any, Dict, List, Optional, Type, TypeVar, Union +from typing import List, Optional, Type, TypeVar, Union import aioarangodb.exceptions import pydantic @@ -23,6 +23,7 @@ MultipleModelsFoundError, UniqueConstraintError, ) +from arangodantic.indexes import BaseIndex from arangodantic.utils import ( FilterTypes, SortTypes, @@ -43,7 +44,7 @@ class ArangodanticCollectionConfig(pydantic.BaseModel): collection_name: Optional[str] = Field( None, description="Override the name of the collection to use" ) - indexes: Optional[Dict[str, List[Dict[str, Any]]]] = Field( + indexes: Optional[List[BaseIndex]] = Field( None, description="Override the collection indexes." ) @@ -248,15 +249,15 @@ async def ensure_indexes(cls): Ensure the collection's indexes are created. The configuration for the indexes are defined in the ArangodanticConfig class - for a model. The class should define an `indexes` dictionary, which contains - the desired indexing function to be run, and it's index definitions. - Supported index functions are: - - add_hash_index - - add_geo_index - - add_ttl_index - - add_fulltext_index - - add_persistent_index - - add_skiplist_index + for a model. The class should define an `indexes` list, which contains + the desired indexing class to be run, and it's index definitions. + Supported index classes are: + - HashIndex (add_hash_index) + - GeoIndex (add_geo_index) + - TTLIndex (add_ttl_index) + - FulltextIndex (add_fulltext_index) + - PersistentIndex (add_persistent_index) + - SkiplistIndex (add_skiplist_index) All field definition key-values are passed to the index function as keyword arguments. @@ -264,14 +265,12 @@ async def ensure_indexes(cls): Example: class Node(DocumentModel): class ArangodanticConfig: - indexes = { - "add_hash_index": [ - {"fields": ["field1", "field2"]}, - {"fields: ["field3"], "unique": True}, - {"fields": ["field4"], "sparse": True}, - {"fields": ["field5"], "deduplicate": True}, - ] - } + indexes = [ + HashIndex(fields=["field1", "field2"]), + HashIndex(fields=["field3"], unique=True), + HashIndex(fields=["field4"], sparse=True), + HashIndex(fields=["field5"], deduplicate=True), + ] """ cls_config: ArangodanticCollectionConfig = getattr( cls, "ArangodanticConfig", ArangodanticCollectionConfig() @@ -282,9 +281,8 @@ class ArangodanticConfig: if indexes: collection = cls.get_collection() - for index_function, index_fields in indexes.items(): - for index_definition in index_fields: - await getattr(collection, index_function)(**index_definition) + for index in indexes: + await index.add_index(collection) @classmethod async def delete_collection(cls, ignore_missing: bool = True): diff --git a/arangodantic/tests/conftest.py b/arangodantic/tests/conftest.py index f014e6c..6b091de 100644 --- a/arangodantic/tests/conftest.py +++ b/arangodantic/tests/conftest.py @@ -11,6 +11,7 @@ from shylock import configure as configure_shylock from arangodantic import DocumentModel, EdgeDefinition, EdgeModel, Graph, configure +from arangodantic.indexes import HashIndex HOSTS = "http://localhost:8529" USERNAME = "root" @@ -52,7 +53,7 @@ class Identity(DocumentModel): """Dummy identity Arangodantic model.""" class ArangodanticConfig: - indexes = {"add_hash_index": [{"fields": ["name"]}]} + indexes = [HashIndex(fields=["name"])] name: str = "" @@ -83,9 +84,7 @@ class Link(EdgeModel): """Dummy Arangodantic edge model.""" class ArangodanticConfig: - indexes = { - "add_hash_index": [{"fields": ["_from", "_to", "type"], "unique": True}] - } + indexes = [HashIndex(fields=["_from", "_to", "type"], unique=True)] type: str diff --git a/examples/graph_example.py b/examples/graph_example.py index 5a4df07..13aecb9 100644 --- a/examples/graph_example.py +++ b/examples/graph_example.py @@ -15,6 +15,7 @@ ModelNotFoundError, configure, ) +from arangodantic.indexes import HashIndex # Define models @@ -22,7 +23,7 @@ class Person(DocumentModel): """Documents describing persons.""" class ArangodanticConfig: - indexes = {"add_hash_index": [{"fields": ["name"]}]} + indexes = [HashIndex(fields=["name"])] name: str @@ -31,7 +32,7 @@ class Relation(EdgeModel): """Edge documents describing relation between people.""" class ArangodanticConfig: - indexes = {"add_hash_index": [{"fields": ["kind"]}]} + indexes = [HashIndex(fields=["kind"])] kind: str diff --git a/examples/readme_example.py b/examples/readme_example.py index a593c96..8442f32 100644 --- a/examples/readme_example.py +++ b/examples/readme_example.py @@ -8,6 +8,7 @@ from shylock import configure as configure_shylock from arangodantic import ASCENDING, DocumentModel, EdgeModel, configure +from arangodantic.indexes import HashIndex # Define models @@ -22,13 +23,11 @@ class Company(DocumentModel): """Dummy company Arangodantic model.""" class ArangodanticConfig: - indexes = { - "add_hash_index": [ - {"fields": ["company_id"], "unique": True}, - {"fields": ["owner.first_name"]}, - {"fields": ["owner.last_name"]}, - ], - } + indexes = [ + HashIndex(fields=["company_id"], unique=True), + HashIndex(fields=["owner.first_name"]), + HashIndex(fields=["owner.last_name"]), + ] company_id: str owner: Owner @@ -38,9 +37,7 @@ class Link(EdgeModel): """Dummy Link Arangodantic model.""" class ArangodanticConfig: - indexes = { - "add_hash_index": [{"fields": ["_from", "_to", "type"], "unique": True}] - } + indexes = [HashIndex(fields=["_from", "_to", "type"], unique=True)] type: str From 53ac53e57b9f5657e44e44bfd73120501f202f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Mon, 9 May 2022 15:21:51 +0300 Subject: [PATCH 3/6] Update README --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 335229b..cc22371 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ from shylock import ShylockAioArangoDBBackend from shylock import configure as configure_shylock from arangodantic import ASCENDING, DocumentModel, EdgeModel, configure +from arangodantic.indexes import HashIndex # Define models @@ -61,13 +62,11 @@ class Company(DocumentModel): """Dummy company Arangodantic model.""" class ArangodanticConfig: - indexes = { - "add_hash_index": [ - {"fields": ["company_id"], "unique": True}, - {"fields": ["owner.first_name"]}, - {"fields": ["owner.last_name"]}, - ], - } + indexes = [ + HashIndex(fields=["company_id"], unique=True), + HashIndex(fields=["owner.first_name"]), + HashIndex(fields=["owner.last_name"]), + ] company_id: str owner: Owner @@ -77,9 +76,7 @@ class Link(EdgeModel): """Dummy Link Arangodantic model.""" class ArangodanticConfig: - indexes = { - "add_hash_index": [{"fields": ["_from", "_to", "type"], "unique": True}] - } + indexes = [HashIndex(fields=["_from", "_to", "type"], unique=True)] type: str From ef730a1fc7ad7737107e8107ce0f5448e69e6d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Mon, 9 May 2022 15:38:08 +0300 Subject: [PATCH 4/6] Remove return value typing Mypy doesn't really understand what is returned from the add_*_index function. --- arangodantic/indexes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arangodantic/indexes.py b/arangodantic/indexes.py index 5d5132f..3c3b093 100644 --- a/arangodantic/indexes.py +++ b/arangodantic/indexes.py @@ -33,7 +33,7 @@ class BaseIndex(pydantic.BaseModel): None, description="Do not hold the collection lock." ) - async def add_index(self, collection: StandardCollection) -> dict: + async def add_index(self, collection: StandardCollection): """ Creates the index on the collection. @@ -66,7 +66,7 @@ class SkiplistIndex(BaseIndex): Creates a skiplist index on the collection. """ - async def add_index(self, collection: StandardCollection) -> dict: + async def add_index(self, collection: StandardCollection): return await collection.add_skiplist_index( fields=self.fields, unique=self.unique, @@ -86,7 +86,7 @@ class GeoIndex(BaseIndex): None, description="Whether the order is longitude, then latitude." ) - async def add_index(self, collection: StandardCollection) -> dict: + async def add_index(self, collection: StandardCollection): return await collection.add_geo_index( fields=self.fields, ordered=self.ordered, @@ -104,7 +104,7 @@ class FulltextIndex(BaseIndex): None, description="Minimum number of characters to index." ) - async def add_index(self, collection: StandardCollection) -> dict: + async def add_index(self, collection: StandardCollection): return await collection.add_fulltext_index( fields=self.fields, min_length=self.min_length, @@ -118,7 +118,7 @@ class PersistentIndex(BaseIndex): Creates a persistent index on the collection. """ - async def add_index(self, collection: StandardCollection) -> dict: + async def add_index(self, collection: StandardCollection): return await collection.add_persistent_index( fields=self.fields, unique=self.unique, @@ -137,7 +137,7 @@ class TTLIndex(BaseIndex): ..., description="Time of expiry in seconds after document creation." ) - async def add_index(self, collection: StandardCollection) -> dict: + async def add_index(self, collection: StandardCollection): return await collection.add_ttl_index( fields=self.fields, expiry_time=self.expiry_time, From 5f8cd6903f1d6d462beb1308024cc1e719e94aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Mon, 9 May 2022 16:39:49 +0300 Subject: [PATCH 5/6] Move index creating to tests from conftest --- arangodantic/tests/conftest.py | 2 -- arangodantic/tests/test_model.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arangodantic/tests/conftest.py b/arangodantic/tests/conftest.py index 6b091de..ee0a9d0 100644 --- a/arangodantic/tests/conftest.py +++ b/arangodantic/tests/conftest.py @@ -132,7 +132,6 @@ class ArangodanticConfig: @pytest.fixture async def identity_collection(configure_db): await Identity.ensure_collection() - await Identity.ensure_indexes() yield await Identity.delete_collection() @@ -161,7 +160,6 @@ async def extended_identity_collection(configure_db): @pytest.fixture async def link_collection(configure_db): await Link.ensure_collection() - await Link.ensure_indexes() yield await Link.delete_collection() diff --git a/arangodantic/tests/test_model.py b/arangodantic/tests/test_model.py index d23dd6e..5cd8947 100644 --- a/arangodantic/tests/test_model.py +++ b/arangodantic/tests/test_model.py @@ -429,6 +429,7 @@ async def test_find_one_with_sort(identity_collection): @pytest.mark.asyncio async def test_identity_indexes(identity_collection): + await Identity.ensure_indexes() collection = Identity.get_collection() for index_definition in await collection.indexes(): if index_definition["type"] in {"primary", "edge"}: @@ -440,6 +441,7 @@ async def test_identity_indexes(identity_collection): @pytest.mark.asyncio async def test_link_indexes(link_collection): + await Link.ensure_indexes() collection = Link.get_collection() for index_definition in await collection.indexes(): if index_definition["type"] in {"primary", "edge"}: From 0dca83be7f8858e1da9777d4279469547f661a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sun, 6 Nov 2022 15:18:23 +0200 Subject: [PATCH 6/6] Ensure Indexes: Fix tests Fix the tests for ensuring indexes. Make BaseIndex abstract base class, and make the add_index function as abstract as well. Move fields away from the BaseIndex to their respective sub-classes. --- arangodantic/indexes.py | 58 +++++++++++++++++++++--------- arangodantic/models.py | 4 +-- arangodantic/tests/conftest.py | 3 ++ arangodantic/tests/test_model.py | 62 +++++++++++++++++++++++--------- 4 files changed, 92 insertions(+), 35 deletions(-) diff --git a/arangodantic/indexes.py b/arangodantic/indexes.py index 3c3b093..8dc8c36 100644 --- a/arangodantic/indexes.py +++ b/arangodantic/indexes.py @@ -1,3 +1,4 @@ +from abc import ABC, abstractmethod from typing import List, Optional import pydantic @@ -5,34 +6,22 @@ from pydantic import Field -class BaseIndex(pydantic.BaseModel): +class BaseIndex(ABC, pydantic.BaseModel): """ Base index model. - Defines fields common to most of the indexes. The index class should be derived - from this base class, and add the create_index() function which actually creates + Define fields common to most of the indexes. The index class should be derived + from this base class, and add the add_index() function which actually creates the index. """ fields: List = Field(..., description="List of fields to index") - unique: Optional[bool] = Field( - None, description="Whether the index is unique or not." - ) - sparse: Optional[bool] = Field( - None, - description="If set to True, documents with None in the field are also indexed," - " otherwise they're skipped", - ) - deduplicate: Optional[bool] = Field( - None, - description="If set to True, inserting duplicate index values from the same " - "document triggers unique constraint errors.", - ) name: Optional[str] = Field(None, description="Optional name for the index.") in_background: Optional[bool] = Field( None, description="Do not hold the collection lock." ) + @abstractmethod async def add_index(self, collection: StandardCollection): """ Creates the index on the collection. @@ -50,6 +39,20 @@ class HashIndex(BaseIndex): Creates a hash index on the collection. """ + unique: Optional[bool] = Field( + None, description="Whether the index is unique or not." + ) + sparse: Optional[bool] = Field( + None, + description="If set to True, documents with None in the field are also indexed," + " otherwise they're skipped", + ) + deduplicate: Optional[bool] = Field( + None, + description="If set to True, inserting duplicate index values from the same " + "document triggers unique constraint errors.", + ) + async def add_index(self, collection: StandardCollection): return await collection.add_hash_index( fields=self.fields, @@ -66,6 +69,20 @@ class SkiplistIndex(BaseIndex): Creates a skiplist index on the collection. """ + unique: Optional[bool] = Field( + None, description="Whether the index is unique or not." + ) + sparse: Optional[bool] = Field( + None, + description="If set to True, documents with None in the field are also indexed," + " otherwise they're skipped", + ) + deduplicate: Optional[bool] = Field( + None, + description="If set to True, inserting duplicate index values from the same " + "document triggers unique constraint errors.", + ) + async def add_index(self, collection: StandardCollection): return await collection.add_skiplist_index( fields=self.fields, @@ -118,6 +135,15 @@ class PersistentIndex(BaseIndex): Creates a persistent index on the collection. """ + unique: Optional[bool] = Field( + None, description="Whether the index is unique or not." + ) + sparse: Optional[bool] = Field( + None, + description="If set to True, documents with None in the field are also indexed," + " otherwise they're skipped", + ) + async def add_index(self, collection: StandardCollection): return await collection.add_persistent_index( fields=self.fields, diff --git a/arangodantic/models.py b/arangodantic/models.py index 4b3b904..6b485d0 100644 --- a/arangodantic/models.py +++ b/arangodantic/models.py @@ -275,9 +275,7 @@ class ArangodanticConfig: cls_config: ArangodanticCollectionConfig = getattr( cls, "ArangodanticConfig", ArangodanticCollectionConfig() ) - indexes = None - if getattr(cls_config, "indexes", None): - indexes = cls_config.indexes + indexes = getattr(cls_config, "indexes", None) if indexes: collection = cls.get_collection() diff --git a/arangodantic/tests/conftest.py b/arangodantic/tests/conftest.py index ee0a9d0..3e3ef74 100644 --- a/arangodantic/tests/conftest.py +++ b/arangodantic/tests/conftest.py @@ -132,6 +132,7 @@ class ArangodanticConfig: @pytest.fixture async def identity_collection(configure_db): await Identity.ensure_collection() + await Identity.ensure_indexes() yield await Identity.delete_collection() @@ -153,6 +154,7 @@ async def identity_bob(identity_collection): @pytest.fixture async def extended_identity_collection(configure_db): await ExtendedIdentity.ensure_collection() + await ExtendedIdentity.ensure_indexes() yield await ExtendedIdentity.delete_collection() @@ -160,6 +162,7 @@ async def extended_identity_collection(configure_db): @pytest.fixture async def link_collection(configure_db): await Link.ensure_collection() + await Link.ensure_indexes() yield await Link.delete_collection() diff --git a/arangodantic/tests/test_model.py b/arangodantic/tests/test_model.py index 5cd8947..8abef6b 100644 --- a/arangodantic/tests/test_model.py +++ b/arangodantic/tests/test_model.py @@ -428,23 +428,53 @@ async def test_find_one_with_sort(identity_collection): @pytest.mark.asyncio -async def test_identity_indexes(identity_collection): - await Identity.ensure_indexes() - collection = Identity.get_collection() - for index_definition in await collection.indexes(): - if index_definition["type"] in {"primary", "edge"}: - continue +async def test_identity_indexes(configure_db): + try: + await Identity.ensure_collection() + collection = Identity.get_collection() + + indexes_before = await collection.indexes() + assert len(indexes_before) == 1 + + # Ensure the indexes are created + await Identity.ensure_indexes() - if index_definition["type"] == "hash": - assert "name" in index_definition["fields"] + indexes_after = await collection.indexes() + assert len(indexes_after) == len(indexes_before) + 1 + + created_indexes = [ + index + for index in indexes_after + if index["type"] == "hash" and index["fields"] == ["name"] + ] + assert len(created_indexes) == 1 + finally: + await Identity.delete_collection() @pytest.mark.asyncio -async def test_link_indexes(link_collection): - await Link.ensure_indexes() - collection = Link.get_collection() - for index_definition in await collection.indexes(): - if index_definition["type"] in {"primary", "edge"}: - continue - if index_definition["type"] == "hash": - assert ["_from", "_to", "type"] == index_definition["fields"] +async def test_link_indexes(configure_db): + try: + await Link.ensure_collection() + collection = Link.get_collection() + + indexes_before = await collection.indexes() + # Here we have the primary index, + # and the _from + _to indexes for edges + assert len(indexes_before) == 2 + + # Ensure the indexes are created + await Link.ensure_indexes() + + indexes_after = await collection.indexes() + assert len(indexes_after) == len(indexes_before) + 1 + + created_indexes = [ + index + for index in indexes_after + if index["type"] == "hash" and index["fields"] == ["_from", "_to", "type"] + ] + assert len(created_indexes) == 1 + + finally: + await Link.delete_collection()