Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/api-reference/orm.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class ForeignKey(Generic[T]):
| `null` | `bool` | `False` | Whether FK can be null |
| `unique` | `bool` | `False` | Whether FK must be unique (one-to-one) |
| `related_name` | `str` | `None` | `None` | Name for reverse relationship (auto-generated if `None`) |
| `db_column` | `str` | `None` | `None` | Custom column name for `_id` field |
| `db_column` | `str` | `None` | `None` | Custom database column for the generated `_id` field |

**Example:**

Expand All @@ -139,6 +139,21 @@ class Book(BaseDBModel):
)
```

When `db_column` is set, runtime operations still use model field names while
SQL is generated against the mapped database column:

```python
class Book(BaseDBModel):
title: str
author: ForeignKey[Author] = ForeignKey(
Author,
db_column="author_ref",
)

# Still uses model field names:
db.select(Book).filter(author_id=1).order("author_id").fetch_all()
```

### `ForeignKeyDescriptor`

> [!CAUTION]
Expand Down
13 changes: 13 additions & 0 deletions docs/guide/foreign-keys/explicit.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ class Book(BaseDBModel):
)
```

When `db_column` is set, your Python API still uses the model field name
(`author_id`). SQLiter maps runtime SQL generation to the configured database
column (`writer_id`) for insert/get/filter/order/update operations.

```python
author = db.insert(Author(name="Jane Austen", email="jane@example.com"))
book = db.insert(Book(title="Emma", author_id=author.pk))

# Uses model field names in Python:
rows = db.select(Book).filter(author_id=author.pk).order("author_id").fetch_all()
db.select(Book).filter(pk=book.pk).update({"author_id": author.pk})
```

## Type Checking

The examples in this documentation show the simplest syntax that works at
Expand Down
28 changes: 26 additions & 2 deletions docs/guide/foreign-keys/orm.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ db.create_table(Book)
> [!NOTE]
>
> When using ORM foreign keys, SQLiter automatically creates an `author_id`
> field in the database. You define `author` (without `_id`) in your model and
> access it for lazy loading.
> model field. By default, the database column is also `author_id`, but you can
> override the physical column name with `db_column=...`.

## Database Context

Expand All @@ -51,6 +51,30 @@ book.db_context = db # Set manually for lazy loading to work
print(book.author.name)
```

## Custom DB Column Names

You can keep ORM-style access (`book.author` / `author_id`) while storing the
FK value in a different database column:

```python
class Author(BaseDBModel):
name: str

class Book(BaseDBModel):
title: str
author: ForeignKey[Author] = ForeignKey(
Author,
db_column="author_ref",
)
```

With this configuration:

- Model-level access still uses `author_id` and `author`
- SQLiter maps runtime CRUD/query operations to `author_ref` in SQL
- Filtering and ordering keep using model field names (for example,
`.filter(author_id=1).order("author_id")`)

## Lazy Loading

When you access a foreign key field, SQLiter automatically loads the related
Expand Down
62 changes: 62 additions & 0 deletions docs/tui-demo/orm.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,68 @@ db.close()
# --8<-- [end:insert-foreign-key]
```

## Custom FK `db_column`

Use a custom DB column name for an ORM FK while keeping the model API on
`author_id`.

```python
# --8<-- [start:custom-fk-db-column]
from sqliter import SqliterDB
from sqliter.orm import BaseDBModel, ForeignKey

class Author(BaseDBModel):
name: str

class Book(BaseDBModel):
title: str
author: ForeignKey[Author] = ForeignKey(
Author,
db_column="author_ref",
related_name="books",
)

db = SqliterDB(memory=True)
db.create_table(Author)
db.create_table(Book)

alice = db.insert(Author(name="Alice"))
bob = db.insert(Author(name="Bob"))
db.insert(Book(title="A1", author=alice))
db.insert(Book(title="A2", author=alice))

rows = (
db.select(Book)
.filter(author_id=alice.pk)
.order("author_id")
.select_related("author")
.fetch_all()
)

print("Books filtered by model field author_id:")
for row in rows:
print(f" {row.title} -> {row.author.name}")

db.select(Book).filter(title="A1").update({"author_id": bob.pk})
updated = db.select(Book).filter(title="A1").fetch_one()

if updated is not None:
print("\nAfter update(author_id=...):")
print(f" {updated.title} -> {updated.author.name}")

print("\nModel API uses author_id while SQL stores it in author_ref.")

db.close()
# --8<-- [end:custom-fk-db-column]
```

### What Happens

- `db_column="author_ref"` customizes only the physical column name
- ORM and QueryBuilder calls still use model field names like `author_id`
- `insert`, `get`, `filter`, `order`, `select_related`, and `update` all map
through to the custom DB column

### Storage vs Access

- **Storage**: The `author` field stores only the primary key (integer)
Expand Down
24 changes: 24 additions & 0 deletions sqliter/model/foreign_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,27 @@ def get_foreign_key_info(field_info: FieldInfo) -> Optional[ForeignKeyInfo]:
if isinstance(fk_info, ForeignKeyInfo):
return fk_info
return None


def get_model_field_db_column(
model_class: type[BaseDBModel], field_name: str
) -> str:
"""Resolve a model field name to its actual database column name.

Args:
model_class: The model class owning the field.
field_name: The model field name (for example ``author_id``).

Returns:
The database column name. For non-FK fields this is the same as
``field_name``; for FK fields with ``db_column`` metadata this returns
the configured column name.
"""
field_info = model_class.model_fields.get(field_name)
if field_info is None:
return field_name

fk_info = get_foreign_key_info(field_info)
if fk_info is None or fk_info.db_column is None:
return field_name
return fk_info.db_column
Loading