2021-09-16 02:41:45 +02:00
import abc
2021-08-31 21:03:53 +02:00
import decimal
import datetime
from typing import Optional , List
2021-10-04 22:55:33 +02:00
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 ( )
2021-10-04 22:55:33 +02:00
today = datetime . date . today ( )
2021-08-31 21:03:53 +02:00
2021-09-16 02:41:45 +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 )
2021-10-04 22:55:33 +02:00
state : str
2021-08-31 21:03:53 +02:00
country : str
2021-10-04 22:55:33 +02:00
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-04 22:55:33 +02:00
name : str = Field ( index = True , full_text_search = 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 ) :
2021-10-06 01:40:02 +02:00
first_name : str = Field ( index = True )
last_name : str = Field ( index = True )
2021-09-17 18:27:11 +02:00
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
2021-09-01 22:13:46 +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
2021-10-04 22:55:33 +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
2021-10-04 22:55:33 +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
2021-10-04 22:55:33 +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 " ,
2021-10-04 22:55:33 +02:00
last_name = " Brookins " ,
2021-08-31 21:03:53 +02:00
zipcode = " 97086 " ,
2021-10-04 22:55:33 +02:00
join_date = today ,
2021-08-31 21:03:53 +02:00
)
2021-10-04 22:55:33 +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 " ,
2021-10-04 22:55:33 +02:00
join_date = " yesterday " ,
address = address
2021-08-31 21:03:53 +02:00
)
# Passes validation
2021-10-04 22:55:33 +02:00
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 " ,
2021-10-04 22:55:33 +02:00
join_date = today ,
age = 38 ,
address = address
2021-08-31 21:03:53 +02:00
)
assert member . first_name == " Andrew "
2021-10-04 22:55:33 +02:00
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 " ,
2021-10-04 22:55:33 +02:00
join_date = today ,
age = 38 ,
address = address
2021-08-31 21:03:53 +02:00
)
2021-10-04 22:55:33 +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 )
2021-10-04 22:55:33 +02:00
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 " )
2021-10-04 22:55:33 +02:00
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 " ,
2021-10-04 22:55:33 +02:00
join_date = today ,
2021-08-31 21:03:53 +02:00
address = address ,
2021-10-04 22:55:33 +02:00
age = 38
2021-08-31 21:03:53 +02:00
) ,
Member (
first_name = " Kim " ,
last_name = " Brookins " ,
email = " k@example.com " ,
2021-10-04 22:55:33 +02:00
join_date = today ,
2021-08-31 21:03:53 +02:00
address = address ,
2021-10-04 22:55:33 +02:00
age = 34
2021-08-31 21:03:53 +02:00
)
]
Member . add ( members )
2021-10-04 22:55:33 +02:00
@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 " )
2021-10-06 01:40:02 +02:00
assert results == members
2021-10-04 22:55:33 +02:00
# 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 )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member2 , member3 ]
2021-10-04 22:55:33 +02:00
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 ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member2 ]
2021-10-12 23:22:57 +02:00
2021-10-04 22:55:33 +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
2021-10-04 22:55:33 +02:00
actual = Member . find ( ~ ( Member . last_name == " Brookins " ) ) . all ( )
assert actual == [ member3 ]
2021-10-12 23:22:57 +02:00
2021-10-04 22:55:33 +02:00
actual = Member . find ( Member . last_name != " Brookins " ) . all ( )
assert actual == [ member3 ]
2021-10-12 23:22:57 +02:00
2021-10-04 22:55:33 +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 ]
2021-10-04 22:55:33 +02:00
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-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 ]
2021-10-04 22:55:33 +02:00
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 ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member2 , member3 ]
2021-10-04 22:55:33 +02:00
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 ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member3 ]
2021-10-04 22:55:33 +02:00
2021-10-06 01:40:02 +02:00
def test_tag_queries_punctuation ( address ) :
member1 = Member (
first_name = " Andrew, the Michael " ,
2021-10-04 22:55:33 +02:00
last_name = " St. Brookins-on-Pier " ,
2021-10-06 01:40:02 +02:00
email = " a|b@example.com " , # NOTE: This string uses the TAG field separator.
2021-10-04 22:55:33 +02:00
age = 38 ,
2021-10-06 01:40:02 +02:00
join_date = today ,
address = address
2021-08-31 22:31:14 +02:00
)
2021-10-06 01:40:02 +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
2021-10-06 01:40:02 +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
2021-10-04 22:55:33 +02:00
def test_tag_queries_negation ( members ) :
member1 , member2 , member3 = members
2021-10-06 01:40:02 +02:00
"""
┌ 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 (
2021-10-04 22:55:33 +02:00
~ ( Member . first_name == " Andrew " ) &
2021-10-06 01:40:02 +02:00
( ( 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 ]
2021-10-04 22:55:33 +02:00
actual = Member . find (
( Member . first_name == " Andrew " ) & ~ ( Member . last_name == " Brookins " ) ) . all ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member3 ]
2021-10-04 22:55:33 +02:00
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 ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member3 ]
2021-10-04 22:55:33 +02:00
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 ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member2 ]
2021-10-04 22:55:33 +02:00
def test_sorting ( members ) :
member1 , member2 , member3 = members
actual = Member . find ( Member . age > 34 ) . sort_by ( ' age ' ) . all ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member1 , member3 ]
2021-10-04 22:55:33 +02:00
actual = Member . find ( Member . age > 34 ) . sort_by ( ' -age ' ) . all ( )
2021-10-06 01:40:02 +02:00
assert actual == [ member3 , member1 ]
2021-10-04 22:55:33 +02:00
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
2021-10-04 22:55:33 +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
2021-10-04 22:55:33 +02:00
def test_schema ( ) :
2021-10-12 23:22:57 +02:00
assert Member . redisearch_schema ( ) == " ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.address.pk AS address_pk TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address__pk TAG SEPARATOR | $.address.note.description AS address__description TAG SEPARATOR | $.orders[].pk AS orders_pk TAG SEPARATOR | $.orders[].items[].pk AS orders_items_pk TAG SEPARATOR | $.orders[].items[].name AS orders_items_name TAG SEPARATOR | $.orders[].items[].name AS orders_items_name_fts TEXT "