Skip to content

Commit 7cbfe2c

Browse files
feat: Add support for various field operations in dynamic queries
- Introduced new field operations such as IN, GREATER_THAN, LESS_THAN, and LIKE to enhance dynamic query generation capabilities. - Updated the documentation to include detailed descriptions and examples of supported field operations. - Modified the `UserRepository` and `ProductRepository` classes to implement new methods for these operations, ensuring consistent usage across repositories. - Enhanced return types for query methods to use `Optional` for better handling of potential null results.
1 parent 9c1137d commit 7cbfe2c

File tree

2 files changed

+175
-23
lines changed

2 files changed

+175
-23
lines changed

README.md

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features
1010
- Automatic CRUD Repository: PySpringModel automatically generates a CRUD repository for each of your SQLModel entities, providing common database operations such as Create, Read, Update, and Delete.
1111
- Managed Sessions: PySpringModel provides a context manager for database sessions, automatically handling session commit and rollback to ensure data consistency.
1212
- Dynamic Query Generation: PySpringModel can dynamically generate and execute SQL queries based on method names in your repositories.
13+
- Field Operations Support: PySpringModel supports various field operations like IN, NOT IN, greater than, less than, LIKE, and more.
1314
- Custom SQL Queries: PySpringModel supports custom SQL queries using the `@Query` decorator for complex database operations.
1415
- RESTful API Integration: PySpringModel integrates with the PySpring framework to automatically generate basic table CRUD APIs for your SQLModel entities.
1516

@@ -50,6 +51,11 @@ class UserRepository(CrudRepository[int, User]):
5051
def find_all_by_status(self, status: str) -> List[User]: ...
5152
def find_all_by_age_and_status(self, age: int, status: str) -> List[User]: ...
5253

54+
# Field operations
55+
def find_all_by_status_in(self, status: List[str]) -> List[User]: ...
56+
def find_by_age_gt(self, age: int) -> Optional[User]: ...
57+
def find_by_name_like(self, name: str) -> Optional[User]: ...
58+
5359
# Custom SQL queries using @Query decorator
5460
@Query("SELECT * FROM user WHERE age > {min_age}")
5561
def find_users_older_than(self, min_age: int) -> List[User]: ...
@@ -123,6 +129,84 @@ def find_all_by_status_or_age(self, status: str, age: int) -> List[User]: ...
123129
def get_all_by_name_or_email(self, name: str, email: str) -> List[User]: ...
124130
```
125131

132+
### Field Operations
133+
134+
PySpringModel supports various field operations for dynamic query generation:
135+
136+
#### Supported Operations
137+
138+
| Operation | Suffix | Description | Example |
139+
|-----------|--------|-------------|---------|
140+
| `EQUALS` | (default) | Field equals value | `find_by_name` |
141+
| `IN` | `_in` | Field in list of values | `find_by_status_in` |
142+
| `GREATER_THAN` | `_gt` | Field greater than value | `find_by_age_gt` |
143+
| `GREATER_EQUAL` | `_gte` | Field greater than or equal to value | `find_by_age_gte` |
144+
| `LESS_THAN` | `_lt` | Field less than value | `find_by_age_lt` |
145+
| `LESS_EQUAL` | `_lte` | Field less than or equal to value | `find_by_age_lte` |
146+
| `LIKE` | `_like` | Field matches pattern | `find_by_name_like` |
147+
| `NOT_EQUALS` | `_ne` | Field not equals value | `find_by_status_ne` |
148+
| `NOT_IN` | `_not_in` | Field not in list of values | `find_by_category_not_in` |
149+
150+
#### Field Operation Examples
151+
152+
```py
153+
class UserRepository(CrudRepository[int, User]):
154+
# IN operations
155+
def find_all_by_status_in(self, status: List[str]) -> List[User]: ...
156+
def find_all_by_id_in(self, id: List[int]) -> List[User]: ...
157+
158+
# Comparison operations
159+
def find_by_age_gt(self, age: int) -> Optional[User]: ...
160+
def find_all_by_age_gte(self, age: int) -> List[User]: ...
161+
def find_by_age_lt(self, age: int) -> Optional[User]: ...
162+
def find_by_age_lte(self, age: int) -> Optional[User]: ...
163+
164+
# Pattern matching
165+
def find_by_name_like(self, name: str) -> Optional[User]: ...
166+
167+
# Negation operations
168+
def find_by_status_ne(self, status: str) -> Optional[User]: ...
169+
def find_by_category_not_in(self, category: List[str]) -> List[User]: ...
170+
171+
# Combined operations
172+
def find_by_age_gt_and_status_in(self, age: int, status: List[str]) -> Optional[User]: ...
173+
def find_by_salary_gte_or_category_in(self, salary: float, category: List[str]) -> Optional[User]: ...
174+
```
175+
176+
#### Usage Examples
177+
178+
```py
179+
# Create repository instance
180+
user_repo = UserRepository()
181+
182+
# IN operations
183+
active_or_pending_users = user_repo.find_all_by_status_in(
184+
status=["active", "pending"]
185+
)
186+
187+
users_by_ids = user_repo.find_all_by_id_in(id=[1, 2, 3, 5])
188+
189+
# Comparison operations
190+
adults = user_repo.find_all_by_age_gte(age=18)
191+
young_users = user_repo.find_by_age_lt(age=25)
192+
senior_users = user_repo.find_by_age_gte(age=65)
193+
194+
# Pattern matching
195+
johns = user_repo.find_by_name_like(name="%John%")
196+
197+
# Negation operations
198+
non_active_users = user_repo.find_by_status_ne(status="active")
199+
non_employees = user_repo.find_by_category_not_in(
200+
category=["employee", "intern"]
201+
)
202+
203+
# Complex combinations
204+
target_users = user_repo.find_by_age_gt_and_status_in(
205+
age=30,
206+
status=["active", "pending"]
207+
)
208+
```
209+
126210
### Custom SQL Queries
127211

128212
For complex queries that can't be expressed through method names, use the `@Query` decorator:
@@ -208,6 +292,11 @@ class UserRepository(CrudRepository[int, User]):
208292
def find_all_by_status(self, status: str) -> List[User]: ...
209293
def find_all_by_age_and_status(self, age: int, status: str) -> List[User]: ...
210294

295+
# Field operations
296+
def find_all_by_status_in(self, status: List[str]) -> List[User]: ...
297+
def find_by_age_gt(self, age: int) -> Optional[User]: ...
298+
def find_by_name_like(self, name: str) -> Optional[User]: ...
299+
211300
# Custom SQL queries
212301
@Query("SELECT * FROM user WHERE age > {min_age}")
213302
def find_users_older_than(self, min_age: int) -> List[User]: ...
@@ -243,9 +332,72 @@ The dynamic query generation follows these naming conventions:
243332
- **Single field**: `find_by_name``WHERE name = ?`
244333
- **Multiple fields with AND**: `find_by_name_and_email``WHERE name = ? AND email = ?`
245334
- **Multiple fields with OR**: `find_by_name_or_email``WHERE name = ? OR email = ?`
246-
- **Return types**:
247-
- `find_by_*` and `get_by_*` return `Optional[Model]`
248-
- `find_all_by_*` and `get_all_by_*` return `List[Model]`
335+
- **Field operations**: `find_by_age_gt``WHERE age > ?`, `find_by_status_in``WHERE status IN (?)`
336+
337+
### Parameter Field Mapping
338+
339+
The system supports both exact matching and common plural-to-singular mapping:
340+
341+
- **Exact Matching**: Parameter names match field names exactly
342+
- **Plural Support**: Parameters can use plural forms (add 's') for better API design
343+
- **Clear Rules**: No ambiguity - parameters must match exactly or be plural forms
344+
- **Helpful Errors**: Clear error messages when mapping fails
345+
346+
```python
347+
class UserRepository(CrudRepository[int, User]):
348+
# Method: find_by_name_and_age
349+
# Fields extracted: ['name', 'age']
350+
351+
# ✅ Valid - Exact parameter names
352+
def find_by_name_and_age(self, name: str, age: int) -> Optional[User]: ...
353+
354+
# ✅ Valid - Different order but same names
355+
def find_by_name_and_age(self, age: int, name: str) -> Optional[User]: ...
356+
357+
# ✅ Valid - Plural parameters for better API design
358+
def find_by_name_and_age(self, names: List[str], ages: List[int]) -> Optional[User]: ...
359+
360+
# ✅ Valid - Mixed singular and plural
361+
def find_by_name_and_age(self, name: str, ages: List[int]) -> Optional[User]: ...
362+
363+
# ❌ Invalid - Wrong parameter names
364+
def find_by_name_and_age(self, username: str, user_age: int) -> Optional[User]: ... # Should be 'name' and 'age'
365+
```
366+
367+
**Return types**:
368+
- `find_by_*` and `get_by_*` return `Optional[Model]`
369+
- `find_all_by_*` and `get_all_by_*` return `List[Model]`
370+
371+
### Quality-Focused Approach
372+
373+
The system balances simplicity with API quality:
374+
375+
**✅ Current Approach (Quality + Simplicity)**
376+
```python
377+
# Method: find_by_name_and_age
378+
# Fields: ['name', 'age']
379+
380+
def find_by_name_and_age(self, name: str, age: int) -> Optional[User]: ...
381+
# ✅ Works: exact parameter names
382+
383+
def find_by_name_and_age(self, names: List[str], ages: List[int]) -> Optional[User]: ...
384+
# ✅ Works: plural parameters for better API design
385+
386+
def find_by_name_and_age(self, username: str, user_age: int) -> Optional[User]: ...
387+
# ❌ Fails: clear error about wrong parameter names
388+
```
389+
390+
**Plural-to-Singular Mapping Rules:**
391+
- **Exact Match First**: If parameter name exists as a field, use it directly
392+
- **Regular Plurals**: `names``name`, `ages``age`
393+
- **Special Cases**: `statuses``status`, `categories``category`
394+
- **No Ambiguity**: Won't map if both singular and plural forms exist as fields
395+
396+
**Key Benefits:**
397+
- **API Quality**: Plural parameters make APIs more intuitive
398+
- **Predictable**: Clear rules about what works and what doesn't
399+
- **Smart Handling**: Properly handles words ending with 's' like 'status'
400+
- **Maintainable**: Simple logic that's easy to understand
249401

250402
### Query Decorator Features
251403

py_spring_model/docs/query_operators.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Field Operations Support in PySpringModel
22

3-
PySpringModel now supports multiple field operations for dynamic query generation, similar to Spring Data JPA. This allows you to find entities using various comparison operators and conditions.
3+
PySpringModel supports multiple field operations for dynamic query generation, similar to Spring Data JPA. This allows you to find entities using various comparison operators and conditions.
44

55
## Supported Field Operations
66

@@ -24,7 +24,7 @@ To use field operations, append the appropriate suffix to your field name in the
2424

2525
```python
2626
from py_spring_model import PySpringModel, Field, CrudRepository
27-
from typing import List
27+
from typing import List, Optional
2828

2929
class User(PySpringModel, table=True):
3030
id: int = Field(default=None, primary_key=True)
@@ -41,16 +41,16 @@ class UserRepository(CrudRepository[int, User]):
4141
def find_all_by_id_in(self, id: List[int]) -> List[User]: ...
4242

4343
# Comparison operations
44-
def find_by_age_gt(self, age: int) -> User: ...
44+
def find_by_age_gt(self, age: int) -> Optional[User]: ...
4545
def find_all_by_age_gte(self, age: int) -> List[User]: ...
46-
def find_by_age_lt(self, age: int) -> List[User]: ...
47-
def find_by_age_lte(self, age: int) -> List[User]: ...
46+
def find_by_age_lt(self, age: int) -> Optional[User]: ...
47+
def find_by_age_lte(self, age: int) -> Optional[User]: ...
4848

4949
# Pattern matching
50-
def find_by_name_like(self, name: str) -> List[User]: ...
50+
def find_by_name_like(self, name: str) -> Optional[User]: ...
5151

5252
# Negation operations
53-
def find_by_status_ne(self, status: str) -> List[User]: ...
53+
def find_by_status_ne(self, status: str) -> Optional[User]: ...
5454
def find_by_category_not_in(self, category: List[str]) -> List[User]: ...
5555
```
5656

@@ -93,7 +93,7 @@ class UserRepository(CrudRepository[int, User]):
9393
self,
9494
age: int,
9595
status: List[str]
96-
) -> List[User]: ...
96+
) -> Optional[User]: ...
9797

9898
# Find users by salary >= amount AND category IN list
9999
def find_by_salary_gte_and_category_in(
@@ -123,14 +123,14 @@ class UserRepository(CrudRepository[int, User]):
123123
self,
124124
age: int,
125125
category: List[str]
126-
) -> List[User]: ...
126+
) -> Optional[User]: ...
127127

128128
# Find users by status NE value OR salary >= amount
129129
def find_by_status_ne_or_salary_gte(
130130
self,
131131
status: str,
132132
salary: float
133-
) -> List[User]: ...
133+
) -> Optional[User]: ...
134134

135135
# Usage
136136
experienced_or_executives = user_repo.find_by_age_gte_or_category_in(
@@ -154,7 +154,7 @@ class UserRepository(CrudRepository[int, User]):
154154
age: int,
155155
status: List[str],
156156
category: List[str]
157-
) -> List[User]: ...
157+
) -> Optional[User]: ...
158158

159159
# Usage
160160
target_users = user_repo.find_by_age_gt_and_status_in_or_category_in(
@@ -269,25 +269,25 @@ class Product(PySpringModel, table=True):
269269
# Define the repository
270270
class ProductRepository(CrudRepository[int, Product]):
271271
# Single field operations
272-
def find_by_price_gt(self, price: float) -> List[Product]: ...
273-
def find_by_price_gte(self, price: float) -> List[Product]: ...
274-
def find_by_stock_lt(self, stock: int) -> List[Product]: ...
275-
def find_by_name_like(self, name: str) -> List[Product]: ...
276-
def find_by_status_ne(self, status: str) -> List[Product]: ...
277-
def find_by_category_in(self, category: List[str]) -> List[Product]: ...
278-
def find_by_category_not_in(self, category: List[str]) -> List[Product]: ...
272+
def find_by_price_gt(self, price: float) -> Optional[Product]: ...
273+
def find_by_price_gte(self, price: float) -> Optional[Product]: ...
274+
def find_by_stock_lt(self, stock: int) -> Optional[Product]: ...
275+
def find_by_name_like(self, name: str) -> Optional[Product]: ...
276+
def find_by_status_ne(self, status: str) -> Optional[Product]: ...
277+
def find_by_category_in(self, category: List[str]) -> Optional[Product]: ...
278+
def find_by_category_not_in(self, category: List[str]) -> Optional[Product]: ...
279279

280280
# Combined operations
281281
def find_by_price_gte_and_status_in(
282282
self,
283283
price: float,
284284
status: List[str]
285-
) -> List[Product]: ...
285+
) -> Optional[Product]: ...
286286
def find_by_stock_lt_or_category_in(
287287
self,
288288
stock: int,
289289
category: List[str]
290-
) -> List[Product]: ...
290+
) -> Optional[Product]: ...
291291

292292
# Setup database
293293
engine = create_engine("sqlite:///:memory:")

0 commit comments

Comments
 (0)