1
1
#!/usr/bin/env python3
2
2
# -*- 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
4
4
5
5
from sqlalchemy import (
6
6
Column ,
@@ -25,6 +25,7 @@ class CRUDPlus(Generic[Model]):
25
25
def __init__ (self , model : Type [Model ]):
26
26
self .model = model
27
27
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
28
29
29
30
def _get_primary_keys (self ) -> list [Column ]:
30
31
"""
@@ -33,29 +34,47 @@ def _get_primary_keys(self) -> list[Column]:
33
34
mapper = inspect (self .model )
34
35
return list (mapper .primary_key )
35
36
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 ]:
37
45
"""
38
46
Validate and normalize primary key input to a dictionary mapping column names to values.
39
47
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.
41
49
: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.
42
51
"""
43
- pk_columns = [pk_col .name for pk_col in self .primary_keys ]
44
52
if len (self .primary_keys ) == 1 :
53
+ pk_col = self ._pk_column_names [0 ]
45
54
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 }
50
59
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
+ )
59
78
60
79
async def create_model (
61
80
self ,
@@ -174,14 +193,16 @@ async def exists(
174
193
async def select_model (
175
194
self ,
176
195
session : AsyncSession ,
177
- pk : Union [Any , Dict [str , Any ]],
196
+ pk : Union [Any , Dict [str , Any ], Tuple [ Any , ...] ],
178
197
* whereclause : ColumnExpressionArgument [bool ],
179
198
) -> Model | None :
180
199
"""
181
200
Query by ID
182
201
183
202
: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.
185
206
:param whereclause: The WHERE clauses to apply to the query.
186
207
:return:
187
208
"""
@@ -289,7 +310,7 @@ async def select_models_order(
289
310
async def update_model (
290
311
self ,
291
312
session : AsyncSession ,
292
- pk : Union [Any , Dict [str , Any ]],
313
+ pk : Union [Any , Dict [str , Any ], Tuple [ Any , ...] ],
293
314
obj : UpdateSchema | dict [str , Any ],
294
315
flush : bool = False ,
295
316
commit : bool = False ,
@@ -299,7 +320,9 @@ async def update_model(
299
320
Update an instance by model's primary key
300
321
301
322
: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.
303
326
:param obj: A pydantic schema or dictionary containing the update data
304
327
:param flush: If `True`, flush all object changes to the database. Default is `False`.
305
328
:param commit: If `True`, commits the transaction immediately. Default is `False`.
@@ -362,15 +385,17 @@ async def update_model_by_column(
362
385
async def delete_model (
363
386
self ,
364
387
session : AsyncSession ,
365
- pk : Union [Any , Dict [str , Any ]],
388
+ pk : Union [Any , Dict [str , Any ], Tuple [ Any , ...] ],
366
389
flush : bool = False ,
367
390
commit : bool = False ,
368
391
) -> int :
369
392
"""
370
393
Delete an instance by model's primary key
371
394
372
395
: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.
374
399
:param flush: If `True`, flush all object changes to the database. Default is `False`.
375
400
:param commit: If `True`, commits the transaction immediately. Default is `False`.
376
401
:return:
0 commit comments