redis-om-python/tests/test_json_model.py

459 lines
13 KiB
Python
Raw Normal View History

import abc
2021-08-31 21:03:53 +02:00
import decimal
import datetime
from typing import Optional, List
from unittest import mock
2021-08-31 21:03:53 +02:00
import pytest
import redis
from pydantic import ValidationError
from redis_developer.orm import (
2021-09-01 21:56:06 +02:00
JsonModel,
2021-08-31 21:03:53 +02:00
Field,
)
2021-10-12 23:22:57 +02:00
from redis_developer.orm.model import RedisModelError, QueryNotSupportedError, NotFoundError, embedded
2021-08-31 21:03:53 +02:00
r = redis.Redis()
today = datetime.date.today()
2021-08-31 21:03:53 +02:00
class BaseJsonModel(JsonModel, abc.ABC):
class Meta:
2021-09-01 21:56:06 +02:00
global_key_prefix = "redis-developer"
2021-08-31 21:03:53 +02:00
2021-10-12 23:22:57 +02:00
class EmbeddedJsonModel(BaseJsonModel, abc.ABC):
class Meta:
embedded = True
class Note(EmbeddedJsonModel):
description: str = Field(index=True)
created_on: datetime.datetime
class Address(EmbeddedJsonModel):
2021-08-31 21:03:53 +02:00
address_line_1: str
address_line_2: Optional[str]
2021-10-12 23:22:57 +02:00
city: str = Field(index=True)
state: str
2021-08-31 21:03:53 +02:00
country: str
postal_code: str = Field(index=True)
2021-10-12 23:22:57 +02:00
note: Optional[Note]
2021-08-31 21:03:53 +02:00
2021-10-12 23:22:57 +02:00
class Item(EmbeddedJsonModel):
2021-09-01 22:06:23 +02:00
price: decimal.Decimal
2021-10-13 17:12:22 +02:00
# name: str = Field(index=True, full_text_search=True)
name: str = Field(index=True)
2021-09-01 22:06:23 +02:00
2021-10-12 23:22:57 +02:00
class Order(EmbeddedJsonModel):
2021-09-01 22:06:23 +02:00
items: List[Item]
2021-08-31 21:03:53 +02:00
total: decimal.Decimal
created_on: datetime.datetime
2021-09-01 21:56:06 +02:00
class Member(BaseJsonModel):
first_name: str = Field(index=True)
last_name: str = Field(index=True)
email: str = Field(index=True)
2021-08-31 21:03:53 +02:00
join_date: datetime.date
2021-10-04 23:04:40 +02:00
age: int = Field(index=True)
2021-08-31 21:03:53 +02:00
# Creates an embedded model.
2021-08-31 21:03:53 +02:00
address: Address
2021-09-01 21:56:06 +02:00
# Creates an embedded list of models.
orders: Optional[List[Order]]
2021-08-31 21:03:53 +02:00
2021-09-01 22:06:23 +02:00
@pytest.fixture()
def address():
yield Address(
address_line_1="1 Main St.",
city="Portland",
state="OR",
country="USA",
postal_code=11111
)
2021-09-01 22:06:23 +02:00
2021-08-31 21:03:53 +02:00
@pytest.fixture()
def members(address):
member1 = Member(
first_name="Andrew",
last_name="Brookins",
email="a@example.com",
age=38,
join_date=today,
address=address
)
member2 = Member(
first_name="Kim",
last_name="Brookins",
email="k@example.com",
age=34,
join_date=today,
address=address
)
2021-08-31 21:03:53 +02:00
member3 = Member(
first_name="Andrew",
last_name="Smith",
email="as@example.com",
age=100,
join_date=today,
address=address
)
member1.save()
member2.save()
member3.save()
yield member1, member2, member3
def test_validates_required_fields(address):
# Raises ValidationError address is required
2021-08-31 21:03:53 +02:00
with pytest.raises(ValidationError):
Member(
first_name="Andrew",
last_name="Brookins",
2021-08-31 21:03:53 +02:00
zipcode="97086",
join_date=today,
2021-08-31 21:03:53 +02:00
)
def test_validates_field(address):
2021-08-31 21:03:53 +02:00
# Raises ValidationError: join_date is not a date
with pytest.raises(ValidationError):
Member(
first_name="Andrew",
last_name="Brookins",
join_date="yesterday",
address=address
2021-08-31 21:03:53 +02:00
)
# Passes validation
def test_validation_passes(address):
2021-08-31 21:03:53 +02:00
member = Member(
first_name="Andrew",
last_name="Brookins",
email="a@example.com",
join_date=today,
age=38,
address=address
2021-08-31 21:03:53 +02:00
)
assert member.first_name == "Andrew"
def test_saves_model_and_creates_pk(address):
2021-08-31 21:03:53 +02:00
member = Member(
first_name="Andrew",
last_name="Brookins",
email="a@example.com",
join_date=today,
age=38,
address=address
2021-08-31 21:03:53 +02:00
)
# Save a model instance to Redis
2021-08-31 21:03:53 +02:00
member.save()
2021-09-01 21:56:06 +02:00
member2 = Member.get(member.pk)
assert member2 == member
2021-09-01 21:56:06 +02:00
assert member2.address == address
2021-08-31 21:03:53 +02:00
2021-09-01 00:52:21 +02:00
@pytest.mark.skip("Not implemented yet")
def test_saves_many(address):
2021-08-31 21:03:53 +02:00
members = [
Member(
first_name="Andrew",
last_name="Brookins",
email="a@example.com",
join_date=today,
2021-08-31 21:03:53 +02:00
address=address,
age=38
2021-08-31 21:03:53 +02:00
),
Member(
first_name="Kim",
last_name="Brookins",
email="k@example.com",
join_date=today,
2021-08-31 21:03:53 +02:00
address=address,
age=34
2021-08-31 21:03:53 +02:00
)
]
Member.add(members)
@pytest.mark.skip("Not ready yet")
def test_updates_a_model(members):
member1, member2, member3 = members
# Or, with an implicit save:
member1.update(last_name="Smith")
assert Member.find(Member.pk == member1.pk).first() == member1
# Or, affecting multiple model instances with an implicit save:
Member.find(Member.last_name == "Brookins").update(last_name="Smith")
results = Member.find(Member.last_name == "Smith")
assert results == members
# Or, updating a field in an embedded model:
member2.update(address__city="Happy Valley")
assert Member.find(Member.pk == member2.pk).first().address.city == "Happy Valley"
def test_paginate_query(members):
member1, member2, member3 = members
actual = Member.find().all(batch_size=1)
assert actual == [member1, member2, member3]
def test_access_result_by_index_cached(members):
member1, member2, member3 = members
query = Member.find().sort_by('age')
# Load the cache, throw away the result.
assert query._model_cache == []
query.execute()
assert query._model_cache == [member2, member1, member3]
# Access an item that should be in the cache.
with mock.patch.object(query.model, 'db') as mock_db:
assert query[0] == member2
assert not mock_db.called
def test_access_result_by_index_not_cached(members):
member1, member2, member3 = members
query = Member.find().sort_by('age')
# Assert that we don't have any models in the cache yet -- we
# haven't made any requests of Redis.
assert query._model_cache == []
assert query[0] == member2
assert query[1] == member1
assert query[2] == member3
def test_exact_match_queries(members):
member1, member2, member3 = members
actual = Member.find(Member.last_name == "Brookins").all()
assert actual == [member1, member2]
2021-10-12 23:22:57 +02:00
actual = Member.find(
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")).all()
assert actual == [member2]
2021-10-12 23:22:57 +02:00
actual = Member.find(~(Member.last_name == "Brookins")).all()
assert actual == [member3]
2021-10-12 23:22:57 +02:00
actual = Member.find(Member.last_name != "Brookins").all()
assert actual == [member3]
2021-10-12 23:22:57 +02:00
actual = Member.find(
(Member.last_name == "Brookins") & (Member.first_name == "Andrew")
| (Member.first_name == "Kim")
).all()
2021-10-12 23:22:57 +02:00
assert actual == [member1, member2]
actual = Member.find(Member.first_name == "Kim", Member.last_name == "Brookins").all()
assert actual == [member2]
actual = Member.find(Member.address.city == "Portland").all()
assert actual == [member1, member2, member3]
2021-10-13 17:12:22 +02:00
def test_recursive_query_expression_resolution(members):
member1, member2, member3 = members
actual = Member.find((Member.last_name == "Brookins") | (
Member.age == 100
) & (Member.last_name == "Smith")).all()
assert actual == [member1, member2, member3]
def test_recursive_query_field_resolution(members):
member1, _, _ = members
2021-10-12 23:22:57 +02:00
member1.address.note = Note(description="Weird house",
created_on=datetime.datetime.now())
member1.save()
actual = Member.find(Member.address.note.description == "Weird house").all()
assert actual == [member1]
member1.orders = [
Order(items=[Item(price=10.99, name="Ball")],
total=10.99,
created_on=datetime.datetime.now())
]
member1.save()
actual = Member.find(Member.orders.items.name == "Ball").all()
assert actual == [member1]
def test_tag_queries_boolean_logic(members):
member1, member2, member3 = members
actual = Member.find(
(Member.first_name == "Andrew") &
(Member.last_name == "Brookins") | (Member.last_name == "Smith")).all()
assert actual == [member1, member3]
def test_tag_queries_punctuation(address):
member1 = Member(
first_name="Andrew, the Michael",
last_name="St. Brookins-on-Pier",
email="a|b@example.com", # NOTE: This string uses the TAG field separator.
age=38,
join_date=today,
address=address
2021-08-31 22:31:14 +02:00
)
member1.save()
member2 = Member(
first_name="Bob",
last_name="the Villain",
email="a|villain@example.com", # NOTE: This string uses the TAG field separator.
age=38,
join_date=today,
address=address
)
member2.save()
assert Member.find(Member.first_name == "Andrew, the Michael").first() == member1
assert Member.find(Member.last_name == "St. Brookins-on-Pier").first() == member1
2021-08-31 21:03:53 +02:00
# Notice that when we index and query multiple values that use the internal
# TAG separator for single-value exact-match fields, like an indexed string,
# the queries will succeed. We apply a workaround that queries for the union
# of the two values separated by the tag separator.
assert Member.find(Member.email == "a|b@example.com").all() == [member1]
assert Member.find(Member.email == "a|villain@example.com").all() == [member2]
2021-08-31 21:03:53 +02:00
def test_tag_queries_negation(members):
member1, member2, member3 = members
"""
first_name
NOT EQ
Andrew
"""
query = Member.find(
~(Member.first_name == "Andrew")
)
assert query.all() == [member2]
"""
first_name
NOT EQ
| Andrew
AND
| last_name
EQ
Brookins
"""
query = Member.find(
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
)
assert query.all() == [member2]
"""
first_name
NOT EQ
| Andrew
AND
| last_name
| EQ
| | Brookins
OR
| last_name
EQ
Smith
"""
query = Member.find(
~(Member.first_name == "Andrew") &
((Member.last_name == "Brookins") | (Member.last_name == "Smith")))
assert query.all() == [member2]
"""
first_name
NOT EQ
| Andrew
AND
| | last_name
| EQ
| Brookins
OR
| last_name
EQ
Smith
"""
query = Member.find(
~(Member.first_name == "Andrew") &
(Member.last_name == "Brookins") | (Member.last_name == "Smith"))
assert query.all() == [member2, member3]
actual = Member.find(
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")).all()
assert actual == [member3]
def test_numeric_queries(members):
member1, member2, member3 = members
actual = Member.find(Member.age == 34).all()
assert actual == [member2]
actual = Member.find(Member.age > 34).all()
assert actual == [member1, member3]
actual = Member.find(Member.age < 35).all()
assert actual == [member2]
actual = Member.find(Member.age <= 34).all()
assert actual == [member2]
actual = Member.find(Member.age >= 100).all()
assert actual == [member3]
actual = Member.find(~(Member.age == 100)).all()
assert actual == [member1, member2]
def test_sorting(members):
member1, member2, member3 = members
actual = Member.find(Member.age > 34).sort_by('age').all()
assert actual == [member1, member3]
actual = Member.find(Member.age > 34).sort_by('-age').all()
assert actual == [member3, member1]
with pytest.raises(QueryNotSupportedError):
# This field does not exist.
Member.find().sort_by('not-a-real-field').all()
with pytest.raises(QueryNotSupportedError):
# This field is not sortable.
Member.find().sort_by('join_date').all()
2021-08-31 21:03:53 +02:00
def test_not_found():
with pytest.raises(NotFoundError):
# This ID does not exist.
Member.get(1000)
2021-08-31 21:03:53 +02:00
def test_schema():
2021-10-13 17:12:22 +02:00
assert Member.redisearch_schema() == "ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | SORTABLE $.first_name AS first_name TAG SEPARATOR | SORTABLE $.last_name AS last_name TAG SEPARATOR | SORTABLE $.email AS email TAG SEPARATOR | SORTABLE $.age AS age NUMERIC SORTABLE $.address.pk AS address_pk TAG SEPARATOR | SORTABLE $.address.city AS address_city TAG SEPARATOR | SORTABLE $.address.postal_code AS address_postal_code TAG SEPARATOR | SORTABLE $.address.note.pk AS address_note_pk TAG SEPARATOR | SORTABLE $.address.note.description AS address_note_description TAG SEPARATOR | SORTABLE $.orders[*].pk AS orders_pk TAG SEPARATOR | SORTABLE $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | SORTABLE $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR | SORTABLE"