Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,56 @@ When working on async support implementation, follow this workflow:
#### Migration Strategy
- Projects can use both sync and async QuerySets in same codebase
- Connection type determines which methods are available
- Clear error messages guide users to correct method usage
- Clear error messages guide users to correct method usage

### Phase 3 Implementation Learnings

#### ReferenceField Async Design
- **AsyncReferenceProxy Pattern**: In async context, ReferenceField returns a proxy object requiring explicit `await proxy.fetch()`
- This prevents accidental sync operations in async code and makes async dereferencing explicit
- The proxy caches fetched values to avoid redundant database calls

#### Field-Level Async Methods
- Async methods should be on the field class, not on proxy instances:
```python
# Correct - call on field class
await AsyncFileDoc.file.async_put(file_obj, instance=doc)

# Incorrect - don't call on proxy instance
await doc.file.async_put(file_obj)
```
- This pattern maintains consistency and avoids confusion with instance methods

#### GridFS Async Implementation
- Use PyMongo's native `gridfs.asynchronous` module instead of Motor
- Key imports: `from gridfs.asynchronous import AsyncGridFSBucket`
- AsyncGridFSProxy handles async file operations (read, delete, replace)
- File operations return sync GridFSProxy for storage in document to maintain compatibility

#### LazyReferenceField Enhancement
- Added `async_fetch()` method directly to LazyReference class
- Maintains same caching behavior as sync version
- Works seamlessly with existing passthrough mode

#### Error Handling Patterns
- GridFS operations need careful error handling for missing files
- Stream position management critical for file reads (always seek(0) after write)
- Grid ID extraction from different proxy types requires type checking

#### Testing Async Fields
- Use separate test classes for each field type for clarity
- Test both positive cases and error conditions
- Always clean up GridFS collections in teardown to avoid test pollution
- Verify proxy behavior separately from actual async operations

#### Known Limitations
- **ListField with ReferenceField**: Currently doesn't auto-convert to AsyncReferenceProxy
- This is a complex case requiring deeper changes to ListField
- Documented as limitation - users need manual async dereferencing for now
- Could be addressed in future enhancement

#### Design Decisions
- **Explicit over Implicit**: Async dereferencing must be explicit via `fetch()` method
- **Proxy Pattern**: Provides clear indication when async operation needed
- **Field-Level Methods**: Consistency with sync API while maintaining async safety
- **Native PyMongo**: Leverage PyMongo's built-in async support rather than external libraries
42 changes: 35 additions & 7 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ async def async_run_in_transaction():
- [x] async_create(), async_update(), async_delete() 벌크 μž‘μ—…
- [x] 비동기 μ»€μ„œ 관리 및 μ΅œμ ν™”

### Phase 3: ν•„λ“œ 및 μ°Έμ‘° (2-3μ£Ό)
- [ ] ReferenceField에 async_fetch() λ©”μ„œλ“œ μΆ”κ°€
- [ ] AsyncReferenceProxy κ΅¬ν˜„
- [ ] LazyReferenceField 비동기 지원
- [ ] GridFS 비동기 μž‘μ—… (async_put, async_get)
- [ ] μΊμŠ€μΌ€μ΄λ“œ μž‘μ—… 비동기화
### Phase 3: ν•„λ“œ 및 μ°Έμ‘° (2-3μ£Ό) βœ… **μ™„λ£Œ** (2025-07-31)
- [x] ReferenceField에 async_fetch() λ©”μ„œλ“œ μΆ”κ°€
- [x] AsyncReferenceProxy κ΅¬ν˜„
- [x] LazyReferenceField 비동기 지원
- [x] GridFS 비동기 μž‘μ—… (async_put, async_get)
- [ ] μΊμŠ€μΌ€μ΄λ“œ μž‘μ—… 비동기화 (Phase 4둜 이동)

### Phase 4: κ³ κΈ‰ κΈ°λŠ₯ (3-4μ£Ό)
- [ ] ν•˜μ΄λΈŒλ¦¬λ“œ μ‹ ν˜Έ μ‹œμŠ€ν…œ κ΅¬ν˜„
Expand Down Expand Up @@ -304,4 +304,32 @@ author = await post.author.async_fetch()
- `async_aggregate()`, `async_distinct()` - κ³ κΈ‰ 집계 κΈ°λŠ₯
- `async_values()`, `async_values_list()` - ν•„λ“œ ν”„λ‘œμ μ…˜
- `async_explain()`, `async_hint()` - 쿼리 μ΅œμ ν™”
- 이듀은 κΈ°λ³Έ 인프라 ꡬ좕 ν›„ ν•„μš”μ‹œ μΆ”κ°€ κ°€λŠ₯
- 이듀은 κΈ°λ³Έ 인프라 ꡬ좕 ν›„ ν•„μš”μ‹œ μΆ”κ°€ κ°€λŠ₯

### Phase 3: Fields and References Async Support (2025-07-31 μ™„λ£Œ)

#### κ΅¬ν˜„ λ‚΄μš©
- **ReferenceField 비동기 지원**: AsyncReferenceProxy νŒ¨ν„΄μœΌλ‘œ μ•ˆμ „ν•œ 비동기 μ°Έμ‘° 처리
- **LazyReferenceField κ°œμ„ **: LazyReference ν΄λž˜μŠ€μ— async_fetch() λ©”μ„œλ“œ μΆ”κ°€
- **GridFS 비동기 μž‘μ—…**: PyMongo의 native async API μ‚¬μš© (gridfs.asynchronous.AsyncGridFSBucket)
- **ν•„λ“œ 레벨 비동기 λ©”μ„œλ“œ**: async_put(), async_get(), async_read(), async_delete(), async_replace()

#### μ£Όμš” μ„±κ³Ό
- 17개 μƒˆλ‘œμš΄ async ν…ŒμŠ€νŠΈ μΆ”κ°€ (μ°Έμ‘°: 8개, GridFS: 9개)
- 총 54개 async ν…ŒμŠ€νŠΈ λͺ¨λ‘ 톡과 (Phase 1: 23, Phase 2: 14, Phase 3: 17)
- PyMongo의 native GridFS async API μ™„λ²½ 톡합
- λͺ…μ‹œμ  async dereferencing으둜 μ•ˆμ „ν•œ μ°Έμ‘° 처리

#### 기술적 세뢀사항
- AsyncReferenceProxy 클래슀둜 async contextμ—μ„œ λͺ…μ‹œμ  fetch() ν•„μš”
- FileField.__get__이 GridFSProxy λ°˜ν™˜, async λ©”μ„œλ“œλŠ” field classμ—μ„œ 호좜
- async_read() μ‹œ stream position reset 처리
- GridFSProxy μΈμŠ€ν„΄μŠ€μ—μ„œ grid_id μΆ”μΆœ 둜직 κ°œμ„ 

#### μ•Œλ €μ§„ μ œν•œμ‚¬ν•­
- ListField λ‚΄ ReferenceFieldλŠ” AsyncReferenceProxy둜 μžλ™ λ³€ν™˜λ˜μ§€ μ•ŠμŒ
- μ΄λŠ” low priority둜 λ¬Έμ„œν™”λ˜μ–΄ 있으며 ν•„μš”μ‹œ ν–₯ν›„ κ°œμ„  κ°€λŠ₯

#### Phase 4둜 μ΄λ™λœ ν•­λͺ©
- μΊμŠ€μΌ€μ΄λ“œ μž‘μ—… (CASCADE, NULLIFY, PULL, DENY) 비동기화
- λ³΅μž‘ν•œ μ°Έμ‘° κ΄€κ³„μ˜ 비동기 처리
23 changes: 23 additions & 0 deletions mongoengine/async_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Async utility functions for MongoEngine async support."""

import contextvars

from mongoengine.connection import (
DEFAULT_CONNECTION_NAME,
ConnectionFailure,
Expand All @@ -10,6 +12,9 @@
is_async_connection,
)

# Context variable for async sessions
_async_session_context = contextvars.ContextVar('mongoengine_async_session', default=None)


async def get_async_collection(collection_name, alias=DEFAULT_CONNECTION_NAME):
"""Get an async collection for the given name and alias.
Expand Down Expand Up @@ -49,6 +54,22 @@ def ensure_sync_connection(alias=DEFAULT_CONNECTION_NAME):
)


async def _get_async_session():
"""Get the current async session if any.

:return: Current async session or None
"""
return _async_session_context.get()


async def _set_async_session(session):
"""Set the current async session.

:param session: The async session to set
"""
_async_session_context.set(session)


async def async_exec_js(code, *args, **kwargs):
"""Execute JavaScript code asynchronously in MongoDB.

Expand Down Expand Up @@ -94,4 +115,6 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
'ensure_sync_connection',
'async_exec_js',
'AsyncContextManager',
'_get_async_session',
'_set_async_session',
]
33 changes: 33 additions & 0 deletions mongoengine/base/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"BaseList",
"EmbeddedDocumentList",
"LazyReference",
"AsyncReferenceProxy",
)


Expand Down Expand Up @@ -472,3 +473,35 @@ def __getattr__(self, name):

def __repr__(self):
return f"<LazyReference({self.document_type}, {self.pk!r})>"

async def async_fetch(self, force=False):
"""Async version of fetch()."""
if not self._cached_doc or force:
self._cached_doc = await self.document_type.objects.async_get(pk=self.pk)
if not self._cached_doc:
raise DoesNotExist("Trying to dereference unknown document %s" % (self))
return self._cached_doc


class AsyncReferenceProxy:
"""Proxy object for async reference field access.

This proxy is returned when accessing a ReferenceField in an async context,
requiring explicit async dereferencing via fetch() method.
"""

__slots__ = ("field", "instance", "_cached_value")

def __init__(self, field, instance):
self.field = field
self.instance = instance
self._cached_value = None

async def fetch(self):
"""Explicitly fetch the referenced document."""
if self._cached_value is None:
self._cached_value = await self.field.async_fetch(self.instance)
return self._cached_value

def __repr__(self):
return f"<AsyncReferenceProxy: {self.field.name} (unfetched)>"
Loading