Field Types¶
Beyond standard Python types (str, int, float, bool, etc.), AMSDAL provides specialized field types for common patterns like file uploads, vector embeddings, and relationships between models.
VectorField¶
VectorField creates a fixed-dimension vector column, useful for storing embeddings from ML models.
from amsdal_models.classes.fields.vector import VectorField
from amsdal.models import Model
class Document(Model):
title: str
embedding: VectorField(768) # 768-dimensional vector
Parameters:
| Parameter | Type | Description |
|---|---|---|
dimensions |
int |
Number of dimensions for the vector |
The field stores a list[float] and includes dimension metadata in the JSON schema for validation.
FileField¶
FileField configures a File-typed field with a specific storage backend.
from amsdal_models.classes.fields.file import FileField
from amsdal.models import Model
from amsdal_models.storage.backends.db import DBStorage
from amsdal.storages.file_system import FileSystemStorage
from amsdal.models.core.file import File
class Attachment(Model):
name: str
# Default storage (from settings)
document: File
# Explicit database storage
avatar: File = FileField(title='Avatar', storage=DBStorage())
# Explicit filesystem storage
report: File = FileField(
title='Report',
storage=FileSystemStorage('/var/media', 'https://cdn.example.com/media/'),
)
# Optional file field
thumbnail: File | None = FileField(default=None, storage=DBStorage())
Parameters:
| Parameter | Type | Description |
|---|---|---|
storage |
Storage \| None |
Storage backend instance. If None, uses the default from settings. |
*args, **kwargs |
Forwarded to ReferenceField — accepts FK kwargs (related_name, db_field, on_delete) plus standard Pydantic Field arguments (default, title, description, …). |
See File Storage for details on the File model and storage backends.
ReferenceField¶
ReferenceField defines a foreign-key relationship to another model. For runtime behavior (resolution, RelatedSet, lakehouse divergence), see Relationships.
from amsdal.models import Model, ReferenceField
from amsdal_models.classes.relationships.enum import ReferenceMode
class Department(Model):
name: str
class Employee(Model):
name: str
department: Department = ReferenceField(..., on_delete=ReferenceMode.CASCADE)
Parameters:
| Parameter | Type | Description |
|---|---|---|
on_delete |
ReferenceMode \| Callable |
Behavior when the referenced object is deleted |
related_name |
str \| None |
Reverse-accessor name installed on the target class. Default None → <reverser_lower>_set. Set to a string to rename (e.g. 'books' → author.books). Set to '+' to disable the reverse accessor. |
db_field |
str \| list[str] \| Callable |
Database column name(s) for the foreign key. Default: {field_name}_{pk_name} where pk_name is the referenced model's PK column (partition_key by default). For composite PKs, defaults to a list[str]. |
*args, **kwargs |
Standard Pydantic Field arguments |
Caveats — soft-deleted FK target¶
A forward FK whose target was soft-deleted behaves inconsistently across backends:
- State backend —
instance.fkends up as a danglingReference(the load fails internally and is swallowed). Accessing a field on it raisesAttributeError. - Lakehouse backend —
instance.fkreturns a tombstone Model with_metadata.is_deleted == True. Access does not raise.
See Relationships — Caveat: soft-deleted FK target for defensive patterns (a safe_fk() helper and a lakehouse hop-directive workaround).
on_delete options¶
from amsdal_models.classes.relationships.enum import ReferenceMode
| Mode | Description |
|---|---|
CASCADE |
Delete this object when the referenced object is deleted |
PROTECT |
Prevent deletion of the referenced object if references exist |
RESTRICT |
Similar to PROTECT, enforced at database level |
SET_NULL |
Set the foreign key to None (field must be optional) |
SET_DEFAULT |
Set the foreign key to its default value |
DO_NOTHING |
Take no action (may cause integrity errors) |
Custom db_field¶
class Employee(Model):
department: Department = ReferenceField(..., db_field='dept_id', on_delete=ReferenceMode.CASCADE)
Reverse accessor naming (related_name)¶
Every forward FK auto-creates a reverse accessor on the target class. The default name is <reverser_lower>_set (e.g. Book → author.book_set). Use related_name to override or disable:
class Book(Model):
author: Author = ReferenceField(related_name='books') # → author.books
class Note(Model):
author: Author = ReferenceField(related_name='+') # disabled
Two FKs that resolve to the same reverse-name on a target raise AmsdalReverseFKConflictError at class-build time — add an explicit related_name to one of them.
For IDE annotations, the new ReverseRelation['Book'] type alias is available. It must be declared under a TYPE_CHECKING guard — at runtime the alias is a no-op generic that Pydantic cannot build a schema from, so an unguarded annotation breaks class build:
from typing import TYPE_CHECKING
from amsdal_models.classes.relationships import ReverseRelation
class Author(Model):
name: str
if TYPE_CHECKING:
# Type-checker hint only — at runtime the reverse accessor is auto-installed
# by the FK on Book; the alias is not evaluated.
books: ReverseRelation['Book']
See Working with Models for usage of the auto-generated reverse accessor.
Nullable / optional FKs¶
To allow the foreign key to be None (and to use SET_NULL), declare the field as optional and pass None as the default:
class Employee(Model):
name: str
department: Department | None = ReferenceField(None, on_delete=ReferenceMode.SET_NULL)
Optional FKs are auto-wrapped in Optional[...] at class-build time.
Composite foreign keys¶
When the referenced model has a composite primary key, provide a list of column names:
class Asset(Model):
__primary_key__: ClassVar[list[str]] = ['asset_name', 'asset_type']
asset_name: str
asset_type: str
class Person(Model):
asset: Asset = ReferenceField(
...,
db_field=['asset_name', 'asset_type'],
on_delete=ReferenceMode.CASCADE,
)
ManyReferenceField¶
ManyReferenceField defines a many-to-many relationship via a through model. For runtime behavior (RelatedSet, custom-through write restrictions, lakehouse snapshotting), see Relationships — Many-to-Many.
from amsdal.models import ManyReferenceField
class Tag(Model):
name: str
class PostTag(Model):
post: 'Post' = ReferenceField(..., db_field='post_id', on_delete=ReferenceMode.CASCADE)
tag: Tag = ReferenceField(..., db_field='tag_id', on_delete=ReferenceMode.CASCADE)
class Post(Model):
title: str
tags: list[Tag] = ManyReferenceField(
through=PostTag,
through_fields=('post', 'tag'),
)
Parameters:
| Parameter | Type | Description |
|---|---|---|
through |
type[Model] |
Through model class (required) |
through_fields |
tuple[str, str] |
Names of the two FK fields on the through model — (source_field, target_field) |
related_name |
str \| None |
Optional reverse-M2M accessor name installed on the target class. Default None leaves no reverse accessor. Set to a string to expose target.<related_name> returning a QuerySet of source models via the through-table. |
*args, **kwargs |
Standard Pydantic Field arguments (default, title, …) |
Opt-in reverse M2M accessor:
class Post(Model):
title: str
tags: list[Tag] = ManyReferenceField(
through=PostTag,
through_fields=('post', 'tag'),
related_name='posts', # → tag.posts returns posts referencing this tag
)
PII Fields¶
PIIStr marks a string field as personally identifiable information. Values are auto-encrypted on save and decrypted on load via the registered crypto service.
from amsdal_models.classes.fields.pii import PIIStr
class User(Model):
name: str
ssn: PIIStr # encrypted at rest, masked in __repr__
email: PIIStr | None = None
PII fields cannot be part of __primary_key__ or any UniqueConstraint (raises PIIInvalidFieldUsageError at class-build time).
See PII Encryption for crypto service registration and the full encryption lifecycle.
Decimal Fields¶
Use a Decimal annotation with Pydantic's max_digits / decimal_places constraints to store exact fixed-precision numbers (money, rates, measurements). Unlike float, values round-trip as decimal.Decimal with no floating-point drift.
from decimal import Decimal
from pydantic import Field
from amsdal.models import Model
class Invoice(Model):
amount: Decimal = Field(max_digits=10, decimal_places=2) # NUMERIC(10, 2)
tax_rate: Decimal = Field(max_digits=5, decimal_places=4) # NUMERIC(5, 4)
total: Decimal # unconstrained NUMERIC
discount: Decimal | None = None # nullable
invoice = Invoice(amount=Decimal('19.99'), tax_rate=Decimal('0.0825'), total=Decimal('21.64'))
invoice.save()
fetched = Invoice.objects.get(amount=Decimal('19.99')).execute()
assert fetched.amount == Decimal('19.99') # exact — no float drift
assert isinstance(fetched.amount, Decimal)
Parameters (standard Pydantic Field constraints):
| Parameter | Type | Description |
|---|---|---|
max_digits |
int |
Total number of significant digits → SQL precision. Omit for an unconstrained decimal. |
decimal_places |
int |
Number of digits after the decimal point → SQL scale. Omit for an unconstrained decimal. |
Pydantic enforces these constraints at runtime — a value exceeding max_digits/decimal_places raises ValidationError.
Storage by backend:
| Backend | Column type | Notes |
|---|---|---|
| PostgreSQL | NUMERIC(precision, scale) |
Native fixed-precision; exact arithmetic and numeric ordering. |
| SQLite | DECIMAL_TEXT(precision, scale) (TEXT affinity) |
Stored as exact text to avoid SQLite's lossy NUMERIC→REAL coercion. |
Note
On SQLite, decimals are stored as text, so order_by and range filters (amount__gt=...) on a decimal column compare lexicographically, not numerically. On PostgreSQL these comparisons are numeric. For numeric ordering/filtering across both backends, prefer PostgreSQL or store an auxiliary sortable value.
Model Meta Options¶
__table_name__¶
Override the default database table name:
from typing import ClassVar
class Person(Model):
__table_name__: ClassVar[str] = 'people'
first_name: str
last_name: str
Without __table_name__, the class name is used as the table name.
__primary_key__¶
Specify custom primary key field(s). By default, AMSDAL adds a partition_key field.
class Person(Model):
__primary_key__: ClassVar[list[str]] = ['person_id']
person_id: int
first_name: str
Composite primary key:
class Enrollment(Model):
__primary_key__: ClassVar[list[str]] = ['student', 'course']
student: 'Student' = ReferenceField(..., on_delete=ReferenceMode.CASCADE)
course: 'Course' = ReferenceField(..., db_field='course_id', on_delete=ReferenceMode.CASCADE)
enrollment_date: date
__constraints__¶
Define unique constraints across one or more fields:
from amsdal.models import UniqueConstraint
class Person(Model):
__constraints__: ClassVar[list[UniqueConstraint]] = [
UniqueConstraint(name='unq_person_email', fields=['email']),
UniqueConstraint(name='unq_person_name', fields=['first_name', 'last_name']),
]
first_name: str
last_name: str
email: str
__indexes__¶
Define database indexes for faster lookups:
from amsdal.models import IndexInfo
class Person(Model):
__indexes__: ClassVar[list[IndexInfo]] = [
IndexInfo(name='idx_person_email', field='email'),
]
email: str
Note
IndexInfo currently supports a single field per entry (field: str). For "composite" needs, declare multiple IndexInfo entries, or use a UniqueConstraint over multiple fields (which also creates a backing unique index).
Combining meta options¶
class Course(Model):
__table_name__: ClassVar[str] = 'courses'
__primary_key__: ClassVar[list[str]] = ['course_id']
__indexes__: ClassVar[list[IndexInfo]] = [
IndexInfo(name='idx_course_name', field='course_name'),
]
__constraints__: ClassVar[list[UniqueConstraint]] = [
UniqueConstraint(name='unq_course_name', fields=['course_name']),
]
course_id: int
course_name: str
description: str = ''