Skip to content

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 backendinstance.fk ends up as a dangling Reference (the load fails internally and is swallowed). Accessing a field on it raises AttributeError.
  • Lakehouse backendinstance.fk returns 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)

Every forward FK auto-creates a reverse accessor on the target class. The default name is <reverser_lower>_set (e.g. Bookauthor.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 NUMERICREAL 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 = ''