redis-om-python/tests/test_json_model.py

364 lines
9.9 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,
)
from redis_developer.orm.model import RedisModelError, QueryNotSupportedError, NotFoundError
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-09-01 21:56:06 +02:00
class Address(BaseJsonModel):
2021-08-31 21:03:53 +02:00
address_line_1: str
address_line_2: Optional[str]
city: str
state: str
2021-08-31 21:03:53 +02:00
country: str
postal_code: str = Field(index=True)
2021-08-31 21:03:53 +02:00
2021-09-01 22:06:23 +02:00
class Item(BaseJsonModel):
price: decimal.Decimal
name: str = Field(index=True, full_text_search=True)
2021-09-01 22:06:23 +02:00
2021-09-01 21:56:06 +02:00
class Order(BaseJsonModel):
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):
2021-08-31 21:03:53 +02:00
first_name: str
last_name: str
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 sorted(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 sorted(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 sorted(actual) == [member1, member2]
actual = Member.find(
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")).all()
assert actual == [member2]
actual = Member.find(~(Member.last_name == "Brookins")).all()
assert actual == [member3]
actual = Member.find(Member.last_name != "Brookins").all()
assert actual == [member3]
actual = Member.find(
(Member.last_name == "Brookins") & (Member.first_name == "Andrew")
| (Member.first_name == "Kim")
).all()
assert actual == [member2, member1]
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]
def test_recursive_query_resolution(members):
member1, member2, member3 = members
actual = Member.find((Member.last_name == "Brookins") | (
Member.age == 100
) & (Member.last_name == "Smith")).all()
assert sorted(actual) == [member1, member2, member3]
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 sorted(actual) == [member1, member3]
def test_tag_queries_punctuation():
2021-08-31 22:31:14 +02:00
member = Member(
first_name="Andrew, the Michael", # This string uses the TAG field separator.
last_name="St. Brookins-on-Pier",
2021-08-31 22:31:14 +02:00
email="a@example.com",
age=38,
2021-08-31 22:31:14 +02:00
join_date=today
)
2021-08-31 21:03:53 +02:00
member.save()
assert Member.find(Member.first_name == "Andrew the Michael").first() == member
assert Member.find(Member.last_name == "St. Brookins-on-Pier").first() == member
assert Member.find(Member.email == "a@example.com").first() == member
2021-08-31 21:03:53 +02:00
def test_tag_queries_negation(members):
member1, member2, member3 = members
actual = Member.find(
~(Member.first_name == "Andrew") &
(Member.last_name == "Brookins") | (Member.last_name == "Smith")).all()
assert sorted(actual) == [member2, member3]
actual = Member.find(
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")).all()
assert sorted(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 sorted(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 sorted(actual) == [member1, member2]
def test_sorting(members):
member1, member2, member3 = members
actual = Member.find(Member.age > 34).sort_by('age').all()
assert sorted(actual) == [member3, member1]
actual = Member.find(Member.age > 34).sort_by('-age').all()
assert sorted(actual) == [member1, member3]
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():
assert Member.redisearch_schema() == "ON JSON PREFIX 1 " \
"redis-developer:tests.test_json_model.Member: " \
"SCHEMA $.pk AS pk TAG " \
"$.email AS email TAG " \
"$.address.pk AS address_pk TAG " \
"$.address.postal_code AS address_postal_code TAG " \
"$.orders[].pk AS orders_pk TAG " \
"$.orders[].items[].pk AS orders_items_pk TAG " \
"$.orders[].items[].name AS orders_items_name TAG " \
"$.orders[].items[].name AS orders_items_name_fts TEXT"