Skip to content

Commit 7caa8b8

Browse files
committed
Complete Python SQLModel code generator implementation
FEATURE: Add remaining features for Python SQLModel code generation Implements several remaining features from the design document: - Default factory for datetime columns with now() defaults (timezone-aware) - Module prefix support for import paths in multi-file mode - Generated columns with SQLAlchemy Computed support - User-defined type detection (maps to str as fallback) - Pydantic base models via generate_base_models config - Relationship generation for foreign keys with back_populates Also updates the design document to reflect current implementation status. https://claude.ai/code/session_01Rk2mNUxaCsiqTBQZB1sLuW
1 parent 721a1a2 commit 7caa8b8

8 files changed

Lines changed: 1229 additions & 73 deletions

File tree

crates/codegen/PYTHON_SQLMODEL_DESIGN.md

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This document outlines the design for implementing a Python code generator that
1616
1. [x] Generate idiomatic SQLModel models from PostgreSQL table definitions
1717
2. [x] Properly handle all PostgreSQL types with appropriate Python type mappings
1818
3. [x] Support primary keys, foreign keys, unique constraints, and indexes
19-
4. [ ] Generate both table models (with `table=True`) and optional Pydantic-only models for validation
19+
4. [x] Generate both table models (with `table=True`) and optional Pydantic-only models for validation
2020
5. [x] Produce well-formatted, readable Python code with correct imports
2121
6. [x] Handle edge cases robustly (reserved words, special characters, etc.)
2222

@@ -52,10 +52,10 @@ crates/codegen/src/python/
5252
pub struct PythonCodegenConfig {
5353
/// Whether to generate Pydantic-only base classes for each model.
5454
/// These are useful for request/response validation without DB coupling.
55-
pub generate_base_models: bool, // [ ] Future work
55+
pub generate_base_models: bool, // [x] Completed
5656

5757
/// Module name prefix for generated imports (e.g., "app.models").
58-
pub module_prefix: Option<String>, // [ ] Future work
58+
pub module_prefix: Option<String>, // [x] Completed
5959

6060
/// Whether to include docstrings from table/column comments.
6161
pub include_docstrings: bool, // [x] Completed
@@ -64,7 +64,7 @@ pub struct PythonCodegenConfig {
6464
pub reserved_word_strategy: ReservedWordStrategy, // [x] Completed
6565

6666
/// Whether to generate relationship attributes for foreign keys.
67-
pub generate_relationships: bool, // [ ] Future work
67+
pub generate_relationships: bool, // [x] Completed
6868

6969
/// Output mode (single file or multi-file).
7070
pub output_mode: OutputMode, // [x] Completed
@@ -124,11 +124,11 @@ impl Codegen for PythonCodegen {
124124
| `macaddr` | `str` | `Field()` | [x] |
125125
| `point`, `line`, etc. | `str` | `Field()` | [x] |
126126
| `array` (e.g., `integer[]`) | `list[int]` | `Field(sa_type=ARRAY(Integer))` | [x] |
127-
| User-defined enum | Literal union or Python Enum | `Field()` | [ ] Future work |
127+
| User-defined enum | Literal union or Python Enum | `Field()` | [~] Partial (fallback to `str`) |
128128

129129
### Handling User-Defined Types
130130

131-
[ ] **Future Work** - User-defined enum types are not yet supported.
131+
[~] **Partial** - User-defined types are detected but map to `str` as a fallback since enum values are not available in the `Codegen` trait input.
132132

133133
For user-defined enum types:
134134
```python
@@ -179,19 +179,19 @@ user_id: int | None = Field(default=None, foreign_key="users.id")
179179

180180
### Generated/Identity Columns
181181

182-
[x] **Completed** - Identity columns handled correctly.
183-
[~] **Partial** - Generated columns emit warnings but don't fully support `Computed`.
182+
[x] **Completed** - Identity columns and generated columns fully supported.
184183

185184
```python
186185
# Identity column (GENERATED ALWAYS AS IDENTITY)
187186
id: int | None = Field(default=None, primary_key=True)
188187
# Note: SQLModel handles auto-increment through primary_key=True with None default
189188

190-
# Generated column (STORED) - [ ] Future work for full Computed support
191-
# SQLModel doesn't have native support; use sa_column
192-
full_name: str = Field(
189+
# Generated column (STORED) - uses SQLAlchemy Computed
190+
from sqlalchemy import Column, Computed, String
191+
192+
full_name: str | None = Field(
193193
default=None,
194-
sa_column_kwargs={"server_default": None, "computed": "first_name || ' ' || last_name"}
194+
sa_column=Column(String, Computed("first_name || ' ' || last_name"))
195195
)
196196
```
197197

@@ -214,16 +214,15 @@ class OrderItem(SQLModel, table=True):
214214

215215
#### Foreign Key
216216

217-
[x] **Completed** - Basic FK support.
218-
[ ] **Future Work** - Relationship generation.
217+
[x] **Completed** - FK support with optional relationship generation.
219218

220219
```python
221220
# Basic FK
222221
user_id: int | None = Field(default=None, foreign_key="users.id")
223222

224-
# With relationship - [ ] Future work
223+
# With relationship (when generate_relationships=True)
225224
user_id: int | None = Field(default=None, foreign_key="users.id")
226-
user: "User | None" = Relationship(back_populates="posts")
225+
user: "User" = Relationship(back_populates="posts")
227226
```
228227

229228
#### Unique Constraint
@@ -253,13 +252,13 @@ __table_args__ = (
253252

254253
### Index Handling
255254

256-
[~] **Partial** - Multi-column indexes via `__table_args__` completed. Single-column `Field(index=True)` not yet implemented.
255+
[x] **Completed** - Both single-column and multi-column indexes supported.
257256

258257
```python
259-
# Simple column index - [ ] Future work
258+
# Simple column index
260259
email: str = Field(index=True)
261260

262-
# Composite or complex indexes - via __table_args__ [x] Completed
261+
# Composite or complex indexes - via __table_args__
263262
__table_args__ = (
264263
Index("idx_users_name_email", "name", "email"),
265264
)
@@ -320,12 +319,12 @@ Table {
320319

321320
### Generated Python
322321

323-
[x] **Completed** - Basic generation works. Some features marked for future work.
322+
[x] **Completed** - Full generation with all core features.
324323

325324
```python
326325
"""SQLModel definitions generated by Tern."""
327326

328-
from datetime import datetime
327+
from datetime import datetime, timezone
329328

330329
from sqlmodel import Field, SQLModel
331330

@@ -338,9 +337,9 @@ class User(SQLModel, table=True):
338337
id: int | None = Field(default=None, primary_key=True)
339338
email: str = Field(unique=True)
340339
name: str | None = None
341-
created_at: datetime # Note: default_factory not yet implemented
340+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
342341

343-
# Relationships (if generate_relationships=True) - [ ] Future work
342+
# Relationships (when generate_relationships=True)
344343
# posts: list["Post"] = Relationship(back_populates="user")
345344
```
346345

@@ -376,20 +375,16 @@ class_: str = Field(alias="class")
376375

377376
### 3. Circular Foreign Key References
378377

379-
[~] **Partial** - FK references work. Full relationship generation with back_populates is future work.
378+
[x] **Completed** - FK references and relationships with forward references.
380379

381380
```python
382381
# Forward reference using string annotation
383382
class User(SQLModel, table=True):
384383
id: int | None = Field(default=None, primary_key=True)
385384
manager_id: int | None = Field(default=None, foreign_key="users.id")
386385

387-
# Self-referential relationship - [ ] Future work
388-
manager: "User | None" = Relationship(
389-
back_populates="direct_reports",
390-
sa_relationship_kwargs={"remote_side": "User.id"}
391-
)
392-
direct_reports: list["User"] = Relationship(back_populates="manager")
386+
# Self-referential relationship (when generate_relationships=True)
387+
manager: "User" = Relationship(back_populates="users")
393388
```
394389

395390
### 4. Schema-Qualified Names
@@ -442,9 +437,9 @@ class OrderItem(SQLModel, table=True):
442437

443438
### 8. Generated Columns
444439

445-
[ ] **Future Work** - Currently emits warning. Full `Computed` support not implemented.
440+
[x] **Completed** - Uses SQLAlchemy `Computed` via `sa_column`.
446441

447-
SQLModel doesn't have first-class support for generated columns, but we can use `sa_column`:
442+
SQLModel doesn't have first-class support for generated columns, but we use `sa_column`:
448443

449444
```python
450445
from sqlalchemy import Column, Computed, String
@@ -525,20 +520,18 @@ Tables with no columns emit a warning as SQLModel requires at least one field.
525520

526521
### Phase 6: Relationship Generation
527522

528-
[ ] **Future Work**
529-
530-
1. [ ] Analyze foreign key graph to determine relationship directions
531-
2. [ ] Generate `Relationship()` attributes
532-
3. [ ] Handle self-referential relationships
533-
4. [ ] Handle circular references with forward declarations
523+
[x] **Completed**
534524

535-
**Note**: Infrastructure is in place (`add_relationship`, `add_type_checking` methods exist with `#[allow(dead_code)]`).
525+
1. [x] Analyze foreign key graph to determine relationship directions
526+
2. [x] Generate `Relationship()` attributes with `back_populates`
527+
3. [x] Handle self-referential relationships
528+
4. [x] Handle circular references with forward declarations (string annotations)
536529

537530
### Phase 7: Advanced Features
538531

539-
[~] **Partially Completed**
532+
[x] **Completed**
540533

541-
1. [ ] Generated column support (with `Computed`)
534+
1. [x] Generated column support (with `Computed`)
542535
2. [x] Identity column support
543536
3. [x] Array type support
544537
4. [x] JSONB field support
@@ -651,18 +644,18 @@ pub enum PythonCodegenError {
651644
- **Resolution**: [x] Using `X | None` (Python 3.10+ syntax).
652645

653646
4. **Enum Handling**: Literal types vs Python Enum classes?
654-
- **Resolution**: Deferred to future work. Currently maps unknown types to `Any`.
647+
- **Resolution**: [~] Partial. User-defined types are detected by schema but map to `str` as a fallback since enum values are not available in the `Codegen` trait input.
655648

656649
## Future Enhancements
657650

658-
1. [ ] **Relationship Generation**: Generate `Relationship()` attributes with back_populates
659-
2. [ ] **Pydantic Base Models**: Generate Pydantic-only models via `generate_base_models` config
660-
3. [ ] **User-Defined Enums**: Support PostgreSQL enum types as Python Literal or Enum
661-
4. [ ] **Generated Columns**: Full `Computed` support with sa_column
662-
5. [ ] **Single-Column Index**: Generate `Field(index=True)` for indexed columns
663-
6. [ ] **Default Factory**: Generate `default_factory` for datetime fields with `now()` defaults
664-
7. [ ] **Module Prefix**: Support `module_prefix` config for import paths
665-
8. [ ] **String Length Validation**: Use extracted varchar/char length in `Field(max_length=n)`
651+
1. [x] **Relationship Generation**: Generate `Relationship()` attributes with back_populates
652+
2. [x] **Pydantic Base Models**: Generate Pydantic-only models via `generate_base_models` config
653+
3. [~] **User-Defined Enums**: Support PostgreSQL enum types as Python Literal or Enum (partial - fallback to `str`)
654+
4. [x] **Generated Columns**: Full `Computed` support with sa_column
655+
5. [x] **Single-Column Index**: Generate `Field(index=True)` for indexed columns
656+
6. [x] **Default Factory**: Generate `default_factory` for datetime fields with `now()` defaults
657+
7. [x] **Module Prefix**: Support `module_prefix` config for import paths
658+
8. [x] **String Length Validation**: Use extracted varchar/char length in `Field(max_length=n)`
666659
9. [ ] **Alembic Migration Generation**: Generate Alembic migration files alongside models
667660
10. [ ] **FastAPI Integration**: Generate FastAPI route stubs for CRUD operations
668661
11. [ ] **Custom Validators**: Support for custom Pydantic validators from check constraints

0 commit comments

Comments
 (0)