Skip to content

Commit d9acb40

Browse files
committed
update use pk in Tuple
1 parent 65e947f commit d9acb40

File tree

4 files changed

+73
-31
lines changed

4 files changed

+73
-31
lines changed

docs/usage/delete_model.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,24 @@ class UserCreate(BaseModel):
6363

6464
async def example(session: AsyncSession):
6565
# Composite primary key model
66-
crud_composite = CRUDPlus(UserComposite)
66+
crud = CRUDPlus(UserComposite)
6767

6868
# Create
69-
await crud_composite.create_model(
69+
await crud.create_model(
7070
session, UserCreate(id="123", name="John", email="[email protected]"), commit=True
7171
)
7272

7373

7474
# Delete by composite primary key (dictionary)
75-
await crud_composite.delete_model(session, {"id": "123", "name": "John"}, commit=True)
75+
await crud.delete_model(session, {"id": "123", "name": "John"}, commit=True)
76+
77+
# Create
78+
await crud.create_model(
79+
session, UserCreate(id="456", name="Jack", email="[email protected]"), commit=True
80+
)
81+
82+
# Delete by composite primary key (tuple)
83+
await crud.delete_model(session, ("456", "Jack"), commit=True)
84+
7685

7786
```

docs/usage/select_model.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,20 @@ class UserCreate(BaseModel):
6161

6262
async def example(session: AsyncSession):
6363
# Composite primary key model
64-
crud_composite = CRUDPlus(UserComposite)
64+
crud = CRUDPlus(UserComposite)
6565

6666
# Create
67-
await crud_composite.create_model(
67+
await crud.create_model(
6868
session, UserCreate(id="123", name="John", email="[email protected]"), commit=True
6969
)
7070

7171
# Select by composite primary key (dictionary)
72-
user = await crud_composite.select_model(session, {"id": "123", "name": "John"})
72+
user = await crud.select_model(session, {"id": "123", "name": "John"})
7373
print(user.email) # [email protected]
7474

75+
# Select by composite primary key (tuple)
76+
user = await crud.select_model(session, ("123", "John"))
77+
print(user.email) # [email protected]
78+
7579

7680
```

docs/usage/update_model.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,21 @@ class UserCreate(BaseModel):
8989

9090
async def example(session: AsyncSession):
9191
# Composite primary key model
92-
crud_composite = CRUDPlus(UserComposite)
92+
crud = CRUDPlus(UserComposite)
9393

9494
# Create
95-
await crud_composite.create_model(
95+
await crud.create_model(
9696
session, UserCreate(id="123", name="John", email="[email protected]"), commit=True
9797
)
9898

9999
# Update by composite primary key (dictionary)
100-
await crud_composite.update_model(
100+
await crud.update_model(
101101
session, {"id": "123", "name": "John"}, {"email": "[email protected]"}, commit=True
102102
)
103103

104+
# Update by composite primary key (tuple)
105+
await crud.update_model(
106+
session, ("123", "John"), {"email": "[email protected]"}, commit=True
107+
)
104108

105109
```

sqlalchemy_crud_plus/crud.py

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3-
from typing import Any, Generic, Iterable, Sequence, Type, Union, Dict
3+
from typing import Any, Generic, Iterable, Sequence, Type, Union, Dict, Tuple
44

55
from sqlalchemy import (
66
Column,
@@ -25,6 +25,7 @@ class CRUDPlus(Generic[Model]):
2525
def __init__(self, model: Type[Model]):
2626
self.model = model
2727
self.primary_key = self._get_primary_key()
28+
self._pk_column_names = [pk_col.name for pk_col in self.primary_keys] # Cache column names
2829

2930
def _get_primary_keys(self) -> list[Column]:
3031
"""
@@ -33,29 +34,47 @@ def _get_primary_keys(self) -> list[Column]:
3334
mapper = inspect(self.model)
3435
return list(mapper.primary_key)
3536

36-
def _validate_pk_input(self, pk: Union[Any, Dict[str, Any]]) -> Dict[str, Any]:
37+
@property
38+
def primary_key_columns(self) -> list[str]:
39+
"""
40+
Return the names of the primary key columns in order.
41+
"""
42+
return self._pk_column_names
43+
44+
def _validate_pk_input(self, pk: Union[Any, Dict[str, Any], Tuple[Any, ...]]) -> Dict[str, Any]:
3745
"""
3846
Validate and normalize primary key input to a dictionary mapping column names to values.
3947
40-
:param pk: A single value for single primary key, or a dictionary for composite primary keys.
48+
:param pk: A single value for single primary key, a dictionary, or a tuple for composite primary keys.
4149
:return: Dictionary mapping primary key column names to their values.
50+
:raises ValueError: If the input format is invalid or missing required primary key columns.
4251
"""
43-
pk_columns = [pk_col.name for pk_col in self.primary_keys]
4452
if len(self.primary_keys) == 1:
53+
pk_col = self._pk_column_names[0]
4554
if isinstance(pk, dict):
46-
if pk_columns[0] not in pk:
47-
raise ValueError(f"Primary key column '{pk_columns[0]}' missing in dictionary")
48-
return {pk_columns[0]: pk[pk_columns[0]]}
49-
return {pk_columns[0]: pk}
55+
if pk_col not in pk:
56+
raise ValueError(f"Primary key column '{pk_col}' missing in dictionary")
57+
return {pk_col: pk[pk_col]}
58+
return {pk_col: pk}
5059
else:
51-
if not isinstance(pk, dict):
52-
raise ValueError(
53-
f"Composite primary keys require a dictionary with keys {pk_columns}, got {type(pk)}"
54-
)
55-
missing = set(pk_columns) - set(pk.keys())
56-
if missing:
57-
raise ValueError(f"Missing primary key columns: {missing}")
58-
return {k: v for k, v in pk.items() if k in pk_columns}
60+
if isinstance(pk, dict):
61+
missing = set(self._pk_column_names) - set(pk.keys())
62+
if missing:
63+
raise ValueError(
64+
f"Missing primary key columns: {missing}. Expected keys: {self._pk_column_names}"
65+
)
66+
return {k: v for k, v in pk.items() if k in self._pk_column_names}
67+
elif isinstance(pk, tuple):
68+
if len(pk) != len(self.primary_keys):
69+
raise ValueError(
70+
f"Expected {len(self.primary_keys)} primary key values, got {len(pk)}. "
71+
f"Expected columns: {self._pk_column_names}"
72+
)
73+
return dict(zip(self._pk_column_names, pk))
74+
raise ValueError(
75+
f"Composite primary keys require a dictionary or tuple with keys/values for {self._pk_column_names}, "
76+
f"got {type(pk)}"
77+
)
5978

6079
async def create_model(
6180
self,
@@ -174,14 +193,16 @@ async def exists(
174193
async def select_model(
175194
self,
176195
session: AsyncSession,
177-
pk: Union[Any, Dict[str, Any]],
196+
pk: Union[Any, Dict[str, Any], Tuple[Any, ...]],
178197
*whereclause: ColumnExpressionArgument[bool],
179198
) -> Model | None:
180199
"""
181200
Query by ID
182201
183202
:param session: The SQLAlchemy async session.
184-
:param pk: The database primary key value.
203+
:param pk: A single value for a single primary key (e.g., int, str), a dictionary
204+
mapping column names to values, or a tuple of values (in column order) for
205+
composite primary keys.
185206
:param whereclause: The WHERE clauses to apply to the query.
186207
:return:
187208
"""
@@ -289,7 +310,7 @@ async def select_models_order(
289310
async def update_model(
290311
self,
291312
session: AsyncSession,
292-
pk: Union[Any, Dict[str, Any]],
313+
pk: Union[Any, Dict[str, Any], Tuple[Any, ...]],
293314
obj: UpdateSchema | dict[str, Any],
294315
flush: bool = False,
295316
commit: bool = False,
@@ -299,7 +320,9 @@ async def update_model(
299320
Update an instance by model's primary key
300321
301322
:param session: The SQLAlchemy async session.
302-
:param pk: The database primary key value.
323+
:param pk: A single value for a single primary key (e.g., int, str), a dictionary
324+
mapping column names to values, or a tuple of values (in column order) for
325+
composite primary keys.
303326
:param obj: A pydantic schema or dictionary containing the update data
304327
:param flush: If `True`, flush all object changes to the database. Default is `False`.
305328
:param commit: If `True`, commits the transaction immediately. Default is `False`.
@@ -362,15 +385,17 @@ async def update_model_by_column(
362385
async def delete_model(
363386
self,
364387
session: AsyncSession,
365-
pk: Union[Any, Dict[str, Any]],
388+
pk: Union[Any, Dict[str, Any], Tuple[Any, ...]],
366389
flush: bool = False,
367390
commit: bool = False,
368391
) -> int:
369392
"""
370393
Delete an instance by model's primary key
371394
372395
:param session: The SQLAlchemy async session.
373-
:param pk: The database primary key value.
396+
:param pk: A single value for a single primary key (e.g., int, str), a dictionary
397+
mapping column names to values, or a tuple of values (in column order) for
398+
composite primary keys.
374399
:param flush: If `True`, flush all object changes to the database. Default is `False`.
375400
:param commit: If `True`, commits the transaction immediately. Default is `False`.
376401
:return:

0 commit comments

Comments
 (0)