Skip to content

Commit 9302aff

Browse files
committed
feat: Add support for composite primary keys in repositories and services, including a fix for MSSQL tuple().in_() syntax.
1 parent 29e6b43 commit 9302aff

4 files changed

Lines changed: 94 additions & 0 deletions

File tree

advanced_alchemy/repository/_sync.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,7 @@ def delete_many(
12471247

12481248
for idx in range(0, len(normalized_ids), effective_chunk_size):
12491249
chunk = normalized_ids[idx : min(idx + effective_chunk_size, len(normalized_ids))]
1250+
# MSSQL doesn't support tuple().in_() syntax, use OR of AND conditions instead
12501251
pk_filter = (
12511252
or_(*[self._build_pk_filter(pk_tuple) for pk_tuple in chunk])
12521253
if self._dialect.name == "mssql"

docs/usage/repositories.rst

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,71 @@ Delete Where
208208
return await repository.delete_where(Post.published.is_(False))
209209
210210
211+
Composite Primary Keys
212+
----------------------
213+
214+
Advanced Alchemy supports models with composite primary keys. For these models, the ``PrimaryKeyType``
215+
accepts several formats when identifying records:
216+
217+
**Tuple Format** - Pass primary key values as a tuple in column definition order:
218+
219+
.. code-block:: python
220+
221+
from advanced_alchemy.repository import SQLAlchemyAsyncRepository
222+
from sqlalchemy import Column, ForeignKey, Integer, String
223+
from sqlalchemy.orm import DeclarativeBase
224+
225+
class Base(DeclarativeBase):
226+
pass
227+
228+
class UserRole(Base):
229+
"""Model with composite primary key."""
230+
__tablename__ = "user_role"
231+
user_id: int = Column(Integer, ForeignKey("user.id"), primary_key=True)
232+
role_id: int = Column(Integer, ForeignKey("role.id"), primary_key=True)
233+
permissions: str = Column(String, nullable=True)
234+
235+
class UserRoleRepository(SQLAlchemyAsyncRepository[UserRole]):
236+
model_type = UserRole
237+
238+
async def get_user_role(db_session, user_id: int, role_id: int) -> UserRole:
239+
repository = UserRoleRepository(session=db_session)
240+
# Tuple format: values in column definition order
241+
return await repository.get((user_id, role_id))
242+
243+
**Dict Format** - Pass primary key values as a dictionary with column names as keys:
244+
245+
.. code-block:: python
246+
247+
async def get_user_role_by_dict(db_session, user_id: int, role_id: int) -> UserRole:
248+
repository = UserRoleRepository(session=db_session)
249+
# Dict format: explicit column names
250+
return await repository.get({"user_id": user_id, "role_id": role_id})
251+
252+
**Bulk Operations with Composite Keys** - Use sequences of tuples or dicts:
253+
254+
.. code-block:: python
255+
256+
async def delete_user_roles(db_session, role_assignments: list[tuple[int, int]]) -> None:
257+
repository = UserRoleRepository(session=db_session)
258+
# Delete multiple records by their composite keys
259+
await repository.delete_many(role_assignments)
260+
261+
async def get_multiple_user_roles(db_session) -> list[UserRole]:
262+
repository = UserRoleRepository(session=db_session)
263+
# Get multiple records using dict format
264+
return await repository.get_many([
265+
{"user_id": 1, "role_id": 5},
266+
{"user_id": 1, "role_id": 6},
267+
{"user_id": 2, "role_id": 5},
268+
])
269+
270+
.. note::
271+
272+
When using tuple format, ensure values are in the same order as the primary key columns
273+
are defined on the model. Dict format is more explicit and avoids ordering issues.
274+
275+
211276
Transaction Management
212277
----------------------
213278

docs/usage/services.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,29 @@ Services provide high-level methods for common operations:
9292
)
9393
return post_service.to_schema(post, schema_type=PostResponse)
9494
95+
Composite Primary Keys
96+
~~~~~~~~~~~~~~~~~~~~~~
97+
98+
Services fully support models with composite primary keys using the same formats as repositories.
99+
Pass primary key values as tuples or dictionaries when using ``get``, ``update``, or ``delete`` methods:
100+
101+
.. code-block:: python
102+
103+
# Get by composite key (tuple format)
104+
user_role = await user_role_service.get((user_id, role_id))
105+
106+
# Update by composite key (dict format)
107+
updated = await user_role_service.update(
108+
data={"permissions": "admin"},
109+
item_id={"user_id": 1, "role_id": 5},
110+
)
111+
112+
# Delete multiple by composite keys
113+
await user_role_service.delete_many([(1, 5), (1, 6), (2, 5)])
114+
115+
See :ref:`Composite Primary Keys <repositories:Composite Primary Keys>` in the Repositories documentation
116+
for more details on supported formats.
117+
95118
Complex Operations
96119
-------------------
97120

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,11 @@ follow_imports = "skip"
471471
ignore_missing_imports = true
472472
module = ["pytest", "pytest.*", "_pytest", "_pytest.*"]
473473

474+
[[tool.mypy.overrides]]
475+
follow_imports = "skip"
476+
ignore_missing_imports = true
477+
module = ["sphinx", "sphinx.*"]
478+
474479
[[tool.mypy.overrides]]
475480
module = "advanced_alchemy._serialization"
476481
warn_unused_ignores = false

0 commit comments

Comments
 (0)