Skip to content

Commit 6e56b60

Browse files
abrookinsclaude
andcommitted
Implement .values() method for dictionary field projection
Add Django-style .values() method to return query results as dictionaries: - .values() returns all fields as dicts - .values('field1', 'field2') returns specific fields as dicts - Uses Redis RETURN clause for efficient field projection - Extensible design supports future .only() method for partial models 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 03945fe commit 6e56b60

File tree

3 files changed

+74
-14
lines changed

3 files changed

+74
-14
lines changed

aredis_om/model/model.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ def __init__(
420420
sort_fields: Optional[List[str]] = None,
421421
projected_fields: Optional[List[str]] = None,
422422
nocontent: bool = False,
423+
return_as_dict: bool = False,
423424
):
424425
if not has_redisearch(model.db()):
425426
raise RedisModelError(
@@ -448,6 +449,8 @@ def __init__(
448449
else:
449450
self.projected_fields = []
450451

452+
self.return_as_dict = return_as_dict
453+
451454
self._expression = None
452455
self._query: Optional[str] = None
453456
self._pagination: List[str] = []
@@ -461,7 +464,9 @@ def dict(self) -> Dict[str, Any]:
461464
limit=self.limit,
462465
expressions=copy(self.expressions),
463466
sort_fields=copy(self.sort_fields),
467+
projected_fields=copy(self.projected_fields),
464468
nocontent=self.nocontent,
469+
return_as_dict=self.return_as_dict,
465470
)
466471

467472
def copy(self, **kwargs):
@@ -946,9 +951,15 @@ async def execute(
946951
return raw_result
947952
count = raw_result[0]
948953

949-
# If we're using field projection, return dictionaries instead of model instances
950-
if self.projected_fields:
951-
results = self._parse_projected_results(raw_result)
954+
# If we're using field projection or explicitly requesting dict output,
955+
# return dictionaries instead of model instances
956+
if self.projected_fields or self.return_as_dict:
957+
if self.projected_fields:
958+
results = self._parse_projected_results(raw_result)
959+
else:
960+
# Return all fields as dicts - need to convert from model instances
961+
model_results = self.model.from_redis(raw_result, self.knn)
962+
results = [model.model_dump() for model in model_results]
952963
else:
953964
results = self.model.from_redis(raw_result, self.knn)
954965
self._model_cache += results
@@ -1005,10 +1016,23 @@ def sort_by(self, *fields: str):
10051016
return self
10061017
return self.copy(sort_fields=list(fields))
10071018

1008-
def return_fields(self, *fields: str):
1019+
def values(self, *fields: str):
1020+
"""
1021+
Return query results as dictionaries instead of model instances.
1022+
1023+
If no fields are specified, returns all fields.
1024+
If fields are specified, returns only those fields.
1025+
1026+
Usage:
1027+
await Model.find().values() # All fields as dicts
1028+
await Model.find().values('name', 'email') # Only specified fields
1029+
"""
10091030
if not fields:
1010-
return self
1011-
return self.copy(projected_fields=list(fields))
1031+
# Return all fields as dicts
1032+
return self.copy(return_as_dict=True)
1033+
else:
1034+
# Return specific fields as dicts
1035+
return self.copy(return_as_dict=True, projected_fields=list(fields))
10121036

10131037
async def update(self, use_transaction=True, **field_values):
10141038
"""

tests/test_hash_model.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,14 +1134,15 @@ class Meta:
11341134

11351135

11361136
@py_test_mark_asyncio
1137-
async def test_return_specified_fields(members, m):
1137+
async def test_values_method_with_specific_fields(members, m):
11381138
member1, member2, member3 = members
1139-
actual = (
1140-
await m.Member.find(
1139+
actual = await (
1140+
m.Member.find(
11411141
(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
11421142
| (m.Member.last_name == "Smith")
11431143
)
1144-
.return_fields("first_name", "last_name")
1144+
.sort_by("last_name")
1145+
.values("first_name", "last_name")
11451146
.all()
11461147
)
11471148
assert actual == [
@@ -1150,6 +1151,23 @@ async def test_return_specified_fields(members, m):
11501151
]
11511152

11521153

1154+
@py_test_mark_asyncio
1155+
async def test_values_method_all_fields(members, m):
1156+
member1, member2, member3 = members
1157+
actual = await m.Member.find(m.Member.first_name == "Andrew").values().all()
1158+
1159+
# Check that it returns all fields as dicts
1160+
assert len(actual) == 2 # Should find Andrew Brookins and Andrew Smith
1161+
# Verify it contains all fields as dictionaries
1162+
for result in actual:
1163+
assert "first_name" in result
1164+
assert "last_name" in result
1165+
assert "email" in result
1166+
assert "age" in result
1167+
assert "pk" in result # Should include primary key
1168+
assert result["first_name"] == "Andrew"
1169+
1170+
11531171
@py_test_mark_asyncio
11541172
async def test_can_search_on_multiple_fields_with_geo_filter(key_prefix, redis):
11551173
class Location(HashModel, index=True):

tests/test_json_model.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -957,14 +957,15 @@ class TypeWithUuid(JsonModel, index=True):
957957

958958

959959
@py_test_mark_asyncio
960-
async def test_return_specified_fields(members, m):
960+
async def test_values_method_with_specific_fields(members, m):
961961
member1, member2, member3 = members
962-
actual = (
963-
await m.Member.find(
962+
actual = await (
963+
m.Member.find(
964964
(m.Member.first_name == "Andrew") & (m.Member.last_name == "Brookins")
965965
| (m.Member.last_name == "Smith")
966966
)
967-
.return_fields("first_name", "last_name")
967+
.sort_by("last_name")
968+
.values("first_name", "last_name")
968969
.all()
969970
)
970971
assert actual == [
@@ -973,6 +974,23 @@ async def test_return_specified_fields(members, m):
973974
]
974975

975976

977+
@py_test_mark_asyncio
978+
async def test_values_method_all_fields(members, m):
979+
member1, member2, member3 = members
980+
actual = await m.Member.find(m.Member.first_name == "Andrew").values().all()
981+
982+
# Check that it returns all fields as dicts
983+
assert len(actual) == 2 # Should find Andrew Brookins and Andrew Smith
984+
# Verify it contains all fields as dictionaries
985+
for result in actual:
986+
assert "first_name" in result
987+
assert "last_name" in result
988+
assert "email" in result
989+
assert "age" in result
990+
assert "pk" in result # Should include primary key
991+
assert result["first_name"] == "Andrew"
992+
993+
976994
@py_test_mark_asyncio
977995
async def test_type_with_enum():
978996
class TestEnum(Enum):

0 commit comments

Comments
 (0)