sqlalchemy-load 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_load-0.2.0/.claude/settings.local.json +10 -0
- sqlalchemy_load-0.2.0/.gitignore +51 -0
- sqlalchemy_load-0.2.0/PKG-INFO +206 -0
- sqlalchemy_load-0.2.0/README.md +194 -0
- sqlalchemy_load-0.2.0/docs/architecture.md +330 -0
- sqlalchemy_load-0.2.0/pyproject.toml +27 -0
- sqlalchemy_load-0.2.0/src/sqlalchemy_load/__init__.py +18 -0
- sqlalchemy_load-0.2.0/src/sqlalchemy_load/cache.py +20 -0
- sqlalchemy_load-0.2.0/src/sqlalchemy_load/errors.py +19 -0
- sqlalchemy_load-0.2.0/src/sqlalchemy_load/generator.py +196 -0
- sqlalchemy_load-0.2.0/src/sqlalchemy_load/parser.py +111 -0
- sqlalchemy_load-0.2.0/tests/conftest.py +76 -0
- sqlalchemy_load-0.2.0/tests/test_generator.py +323 -0
- sqlalchemy_load-0.2.0/tests/test_parser.py +260 -0
- sqlalchemy_load-0.2.0/todo.md +20 -0
- sqlalchemy_load-0.2.0/uv.lock +415 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
htmlcov/
|
|
33
|
+
.tox/
|
|
34
|
+
.nox/
|
|
35
|
+
|
|
36
|
+
# IDE
|
|
37
|
+
.idea/
|
|
38
|
+
.vscode/
|
|
39
|
+
*.swp
|
|
40
|
+
*.swo
|
|
41
|
+
*~
|
|
42
|
+
|
|
43
|
+
# Distribution
|
|
44
|
+
*.manifest
|
|
45
|
+
*.spec
|
|
46
|
+
|
|
47
|
+
# mypy
|
|
48
|
+
.mypy_cache/
|
|
49
|
+
|
|
50
|
+
# ruff
|
|
51
|
+
.ruff_cache/
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-load
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Generate SQLAlchemy query optimization options from simplified field selection syntax
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# SQLAlchemy Load Generator
|
|
14
|
+
|
|
15
|
+
Generate SQLAlchemy query optimization options (`selectinload` + `load_only`) from simplified field selection syntax.
|
|
16
|
+
|
|
17
|
+
## Why This Library?
|
|
18
|
+
|
|
19
|
+
SQLAlchemy's query options (`selectinload`, `joinedload`, `load_only`) are powerful but **painful to write**, especially with nested relationships.
|
|
20
|
+
|
|
21
|
+
### The Problem
|
|
22
|
+
|
|
23
|
+
**1. Verbose nested syntax**
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# Loading User -> Posts -> Comments requires deep nesting
|
|
27
|
+
stmt = select(User).options(
|
|
28
|
+
selectinload(User.posts).options(
|
|
29
|
+
load_only(Post.id, Post.title),
|
|
30
|
+
selectinload(Post.comments).options(
|
|
31
|
+
load_only(Comment.id, Comment.content)
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**2. Coupled with query logic**
|
|
38
|
+
|
|
39
|
+
You must decide what to load at query time, mixing data requirements with query construction. Different API endpoints need different loading strategies, leading to duplicated query code.
|
|
40
|
+
|
|
41
|
+
**3. Dynamic composition is awkward**
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Conditionally adding options requires extra logic
|
|
45
|
+
options = []
|
|
46
|
+
if need_posts:
|
|
47
|
+
options.append(selectinload(User.posts))
|
|
48
|
+
if need_comments:
|
|
49
|
+
options.append(selectinload(User.posts).selectinload(Post.comments))
|
|
50
|
+
stmt = select(User).options(*options)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**4. Easy to cause N+1 or over-fetching**
|
|
54
|
+
|
|
55
|
+
- Forget `selectinload` → N+1 queries
|
|
56
|
+
- Load unnecessary fields → wasted memory
|
|
57
|
+
|
|
58
|
+
### The Solution
|
|
59
|
+
|
|
60
|
+
This library provides a **declarative syntax** similar to GraphQL:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Before: verbose, nested, error-prone
|
|
64
|
+
stmt = select(User).options(
|
|
65
|
+
selectinload(User.posts).options(
|
|
66
|
+
load_only(Post.id, Post.title),
|
|
67
|
+
selectinload(Post.comments).options(
|
|
68
|
+
load_only(Comment.id, Comment.content)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# After: clean, declarative, optimized
|
|
74
|
+
generator = LoadGenerator(Base)
|
|
75
|
+
options = generator.generate(User, "{ id name posts { title comments { content } } }")
|
|
76
|
+
stmt = select(User).options(*options)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install sqlalchemy-load
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from sqlalchemy import select
|
|
89
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
90
|
+
from sqlalchemy_load import LoadGenerator
|
|
91
|
+
|
|
92
|
+
# Initialize with your DeclarativeBase
|
|
93
|
+
generator = LoadGenerator(Base)
|
|
94
|
+
|
|
95
|
+
# Generate options using simplified syntax
|
|
96
|
+
options = generator.generate(User, "{ id name posts { title comments { content } } }")
|
|
97
|
+
|
|
98
|
+
# Use with SQLAlchemy query
|
|
99
|
+
stmt = select(User).options(*options)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Syntax
|
|
103
|
+
|
|
104
|
+
The simplified syntax is similar to GraphQL but without commas:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
{ field1 field2 relationship { nested_field } }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- Fields are space-separated
|
|
111
|
+
- Relationships use `{ }` for nested selection
|
|
112
|
+
- Commas are optional and ignored
|
|
113
|
+
|
|
114
|
+
### Examples
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Simple fields
|
|
118
|
+
generator.generate(User, "{ id name email }")
|
|
119
|
+
|
|
120
|
+
# Nested relationships
|
|
121
|
+
generator.generate(User, "{ id posts { title content } }")
|
|
122
|
+
|
|
123
|
+
# Deeply nested
|
|
124
|
+
generator.generate(User, "{ id posts { title comments { content author } } }")
|
|
125
|
+
|
|
126
|
+
# Multiple relationships
|
|
127
|
+
generator.generate(User, "{ name posts { title } profile { bio } }")
|
|
128
|
+
|
|
129
|
+
# Different models with same generator
|
|
130
|
+
generator.generate(Post, "{ title content author { name } }")
|
|
131
|
+
generator.generate(Comment, "{ content post { title } }")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## API
|
|
135
|
+
|
|
136
|
+
### `LoadGenerator(base_class)`
|
|
137
|
+
|
|
138
|
+
Create a generator with a SQLAlchemy `DeclarativeBase`. Preloads metadata for all models in the registry for optimal performance.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
142
|
+
|
|
143
|
+
class Base(DeclarativeBase):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
generator = LoadGenerator(Base)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `generator.generate(model_class, query_string) -> list`
|
|
150
|
+
|
|
151
|
+
Generate SQLAlchemy options from a query string.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
options = generator.generate(User, "{ id name posts { title } }")
|
|
155
|
+
stmt = select(User).options(*options)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Features
|
|
159
|
+
|
|
160
|
+
- **Preloaded metadata**: All model metadata is cached at initialization for fast lookups
|
|
161
|
+
- **Result caching**: Same query returns cached result, avoiding redundant computation
|
|
162
|
+
- **Parse caching**: Query string parsing is cached with `lru_cache`
|
|
163
|
+
- **Automatic primary key inclusion**: Primary keys are always included in `load_only`
|
|
164
|
+
- **Relationship detection**: Automatically detects SQLAlchemy relationships
|
|
165
|
+
- **Nested loading**: Recursively generates `selectinload` with nested `load_only`
|
|
166
|
+
- **Error handling**: Clear errors for invalid fields, relationships, or syntax
|
|
167
|
+
|
|
168
|
+
## Error Handling
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from sqlalchemy_load import (
|
|
172
|
+
LoadGenerator,
|
|
173
|
+
ParseError,
|
|
174
|
+
FieldNotFoundError,
|
|
175
|
+
RelationshipNotFoundError,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
generator = LoadGenerator(Base)
|
|
179
|
+
|
|
180
|
+
# Syntax error
|
|
181
|
+
try:
|
|
182
|
+
generator.generate(User, "{ id name") # Missing closing brace
|
|
183
|
+
except ParseError as e:
|
|
184
|
+
print(f"Syntax error: {e}")
|
|
185
|
+
|
|
186
|
+
# Field doesn't exist
|
|
187
|
+
try:
|
|
188
|
+
generator.generate(User, "{ nonexistent }")
|
|
189
|
+
except FieldNotFoundError as e:
|
|
190
|
+
print(f"Field not found: {e}")
|
|
191
|
+
|
|
192
|
+
# Relationship doesn't exist
|
|
193
|
+
try:
|
|
194
|
+
generator.generate(User, "{ notarelationship { id } }")
|
|
195
|
+
except RelationshipNotFoundError as e:
|
|
196
|
+
print(f"Relationship not found: {e}")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Requirements
|
|
200
|
+
|
|
201
|
+
- Python >= 3.10
|
|
202
|
+
- SQLAlchemy >= 2.0
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# SQLAlchemy Load Generator
|
|
2
|
+
|
|
3
|
+
Generate SQLAlchemy query optimization options (`selectinload` + `load_only`) from simplified field selection syntax.
|
|
4
|
+
|
|
5
|
+
## Why This Library?
|
|
6
|
+
|
|
7
|
+
SQLAlchemy's query options (`selectinload`, `joinedload`, `load_only`) are powerful but **painful to write**, especially with nested relationships.
|
|
8
|
+
|
|
9
|
+
### The Problem
|
|
10
|
+
|
|
11
|
+
**1. Verbose nested syntax**
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# Loading User -> Posts -> Comments requires deep nesting
|
|
15
|
+
stmt = select(User).options(
|
|
16
|
+
selectinload(User.posts).options(
|
|
17
|
+
load_only(Post.id, Post.title),
|
|
18
|
+
selectinload(Post.comments).options(
|
|
19
|
+
load_only(Comment.id, Comment.content)
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**2. Coupled with query logic**
|
|
26
|
+
|
|
27
|
+
You must decide what to load at query time, mixing data requirements with query construction. Different API endpoints need different loading strategies, leading to duplicated query code.
|
|
28
|
+
|
|
29
|
+
**3. Dynamic composition is awkward**
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# Conditionally adding options requires extra logic
|
|
33
|
+
options = []
|
|
34
|
+
if need_posts:
|
|
35
|
+
options.append(selectinload(User.posts))
|
|
36
|
+
if need_comments:
|
|
37
|
+
options.append(selectinload(User.posts).selectinload(Post.comments))
|
|
38
|
+
stmt = select(User).options(*options)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**4. Easy to cause N+1 or over-fetching**
|
|
42
|
+
|
|
43
|
+
- Forget `selectinload` → N+1 queries
|
|
44
|
+
- Load unnecessary fields → wasted memory
|
|
45
|
+
|
|
46
|
+
### The Solution
|
|
47
|
+
|
|
48
|
+
This library provides a **declarative syntax** similar to GraphQL:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# Before: verbose, nested, error-prone
|
|
52
|
+
stmt = select(User).options(
|
|
53
|
+
selectinload(User.posts).options(
|
|
54
|
+
load_only(Post.id, Post.title),
|
|
55
|
+
selectinload(Post.comments).options(
|
|
56
|
+
load_only(Comment.id, Comment.content)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# After: clean, declarative, optimized
|
|
62
|
+
generator = LoadGenerator(Base)
|
|
63
|
+
options = generator.generate(User, "{ id name posts { title comments { content } } }")
|
|
64
|
+
stmt = select(User).options(*options)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install sqlalchemy-load
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from sqlalchemy import select
|
|
77
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
78
|
+
from sqlalchemy_load import LoadGenerator
|
|
79
|
+
|
|
80
|
+
# Initialize with your DeclarativeBase
|
|
81
|
+
generator = LoadGenerator(Base)
|
|
82
|
+
|
|
83
|
+
# Generate options using simplified syntax
|
|
84
|
+
options = generator.generate(User, "{ id name posts { title comments { content } } }")
|
|
85
|
+
|
|
86
|
+
# Use with SQLAlchemy query
|
|
87
|
+
stmt = select(User).options(*options)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Syntax
|
|
91
|
+
|
|
92
|
+
The simplified syntax is similar to GraphQL but without commas:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
{ field1 field2 relationship { nested_field } }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Fields are space-separated
|
|
99
|
+
- Relationships use `{ }` for nested selection
|
|
100
|
+
- Commas are optional and ignored
|
|
101
|
+
|
|
102
|
+
### Examples
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Simple fields
|
|
106
|
+
generator.generate(User, "{ id name email }")
|
|
107
|
+
|
|
108
|
+
# Nested relationships
|
|
109
|
+
generator.generate(User, "{ id posts { title content } }")
|
|
110
|
+
|
|
111
|
+
# Deeply nested
|
|
112
|
+
generator.generate(User, "{ id posts { title comments { content author } } }")
|
|
113
|
+
|
|
114
|
+
# Multiple relationships
|
|
115
|
+
generator.generate(User, "{ name posts { title } profile { bio } }")
|
|
116
|
+
|
|
117
|
+
# Different models with same generator
|
|
118
|
+
generator.generate(Post, "{ title content author { name } }")
|
|
119
|
+
generator.generate(Comment, "{ content post { title } }")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### `LoadGenerator(base_class)`
|
|
125
|
+
|
|
126
|
+
Create a generator with a SQLAlchemy `DeclarativeBase`. Preloads metadata for all models in the registry for optimal performance.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
130
|
+
|
|
131
|
+
class Base(DeclarativeBase):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
generator = LoadGenerator(Base)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `generator.generate(model_class, query_string) -> list`
|
|
138
|
+
|
|
139
|
+
Generate SQLAlchemy options from a query string.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
options = generator.generate(User, "{ id name posts { title } }")
|
|
143
|
+
stmt = select(User).options(*options)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Features
|
|
147
|
+
|
|
148
|
+
- **Preloaded metadata**: All model metadata is cached at initialization for fast lookups
|
|
149
|
+
- **Result caching**: Same query returns cached result, avoiding redundant computation
|
|
150
|
+
- **Parse caching**: Query string parsing is cached with `lru_cache`
|
|
151
|
+
- **Automatic primary key inclusion**: Primary keys are always included in `load_only`
|
|
152
|
+
- **Relationship detection**: Automatically detects SQLAlchemy relationships
|
|
153
|
+
- **Nested loading**: Recursively generates `selectinload` with nested `load_only`
|
|
154
|
+
- **Error handling**: Clear errors for invalid fields, relationships, or syntax
|
|
155
|
+
|
|
156
|
+
## Error Handling
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from sqlalchemy_load import (
|
|
160
|
+
LoadGenerator,
|
|
161
|
+
ParseError,
|
|
162
|
+
FieldNotFoundError,
|
|
163
|
+
RelationshipNotFoundError,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
generator = LoadGenerator(Base)
|
|
167
|
+
|
|
168
|
+
# Syntax error
|
|
169
|
+
try:
|
|
170
|
+
generator.generate(User, "{ id name") # Missing closing brace
|
|
171
|
+
except ParseError as e:
|
|
172
|
+
print(f"Syntax error: {e}")
|
|
173
|
+
|
|
174
|
+
# Field doesn't exist
|
|
175
|
+
try:
|
|
176
|
+
generator.generate(User, "{ nonexistent }")
|
|
177
|
+
except FieldNotFoundError as e:
|
|
178
|
+
print(f"Field not found: {e}")
|
|
179
|
+
|
|
180
|
+
# Relationship doesn't exist
|
|
181
|
+
try:
|
|
182
|
+
generator.generate(User, "{ notarelationship { id } }")
|
|
183
|
+
except RelationshipNotFoundError as e:
|
|
184
|
+
print(f"Relationship not found: {e}")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Requirements
|
|
188
|
+
|
|
189
|
+
- Python >= 3.10
|
|
190
|
+
- SQLAlchemy >= 2.0
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|