Model Definition¶
Define your data models as Python classes by inheriting from Model. Each model maps to a database table — AMSDAL handles schema generation, migrations, validation, and querying automatically.
from amsdal.models import Model
class Person(Model):
first_name: str
last_name: str
This generates:
CREATE TABLE Person (
partition_key TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
PRIMARY KEY (partition_key)
)
Models are built on top of Pydantic, so you get runtime type validation, serialization, and all Pydantic features out of the box.
You can import Model and related classes from amsdal.models.
Defining Fields¶
Required Fields¶
By default, fields without a default value are required:
from amsdal.models import Model
class Person(Model):
first_name: str
last_name: str
Optional Fields¶
Use Optional or the | None syntax to allow None values:
from typing import Optional
from amsdal.models import Model
class Person(Model):
first_name: Optional[str]
last_name: Optional[str]
from amsdal.models import Model
class Person(Model):
first_name: str | None
last_name: str | None
Default Values¶
Assign defaults directly or use Pydantic's Field:
from amsdal.models import Model
class Person(Model):
first_name: str = 'John'
last_name: str = 'Doe'
from pydantic import Field
from amsdal.models import Model
class Person(Model):
first_name: str = Field(default='John')
last_name: str = Field(default='Doe')
PII Fields¶
For personally identifiable information that must be encrypted at rest, use PIIStr:
from amsdal_models.classes.fields.pii import PIIStr
class Person(Model):
first_name: str
last_name: str
ssn: PIIStr # encrypted at rest, masked in __repr__
See PII Encryption for full setup (crypto service registration, encryption lifecycle).
Relationships¶
Many-to-One¶
Reference another model directly as a field type:
from amsdal.models import Model
class Asset(Model):
asset_name: str
class Person(Model):
first_name: str
last_name: str
asset: Asset
This creates a foreign key — by default the database column is named asset_partition_key.
To customize the foreign key column name, use ReferenceField:
from amsdal.models import Model, ReferenceField
class Person(Model):
first_name: str
last_name: str
asset: Asset = ReferenceField(..., db_field='asset_id')
ReferenceField is a factory that produces a ReferenceFieldInfo (a subclass of Pydantic's FieldInfo). It accepts all standard Field() arguments plus FK-specific kwargs (db_field, on_delete, related_name).
Reverse accessor¶
Whenever a forward FK is declared, the target class auto-receives a reverse accessor (<class_lower>_set by default). Use related_name to rename or disable it:
class Person(Model):
asset: Asset = ReferenceField(related_name='owners') # → asset.owners
# related_name='+' disables the reverse accessor entirely
See Field Types — Reverse accessor naming for full details.
Composite Foreign Keys¶
When the referenced model has a composite primary key:
from amsdal.models import Model, ReferenceField
class Asset(Model):
__primary_key__ = ['asset_name', 'asset_type']
asset_name: str
asset_type: str
class Person(Model):
first_name: str
last_name: str
asset: Asset = ReferenceField(..., db_field=['asset_name', 'asset_type'])
Generated schema:
CREATE TABLE Person (
partition_key TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
asset_name TEXT,
asset_type TEXT,
PRIMARY KEY (partition_key),
FOREIGN KEY (asset_name, asset_type) REFERENCES Asset(asset_name, asset_type)
)
On Delete Behavior¶
Control what happens when a referenced object is deleted using the on_delete parameter:
from amsdal.models import Model, ReferenceField
from amsdal_models.classes.relationships.enum import ReferenceMode
class Person(Model):
first_name: str
last_name: str
asset: Asset = ReferenceField(..., on_delete=ReferenceMode.CASCADE)
Available modes:
| Mode | Behavior |
|---|---|
CASCADE |
Delete this object too |
PROTECT |
Prevent deletion of the referenced object |
RESTRICT |
Similar to PROTECT, checked at database level |
SET_NULL |
Set the foreign key to NULL |
SET_DEFAULT |
Set the foreign key to its default value |
DO_NOTHING |
Do nothing (may cause integrity errors) |
Many-to-Many¶
Use list[Model] to define a many-to-many relationship:
from amsdal.models import Model
class Asset(Model):
name: str
class Person(Model):
first_name: str
last_name: str
assets: list[Asset]
AMSDAL auto-generates a junction table:
CREATE TABLE Asset (
partition_key TEXT NOT NULL,
name TEXT,
PRIMARY KEY (partition_key)
)
CREATE TABLE Person (
partition_key TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
PRIMARY KEY (partition_key)
)
CREATE TABLE PersonAssets (
person_partition_key TEXT NOT NULL,
asset_partition_key TEXT NOT NULL,
PRIMARY KEY (person_partition_key, asset_partition_key),
FOREIGN KEY (person_partition_key) REFERENCES Person(partition_key),
FOREIGN KEY (asset_partition_key) REFERENCES Asset(partition_key)
)
The auto-generated through table is named <ModelName><CamelCaseFieldName> — derived from the M2M field name, not the target class name. So Person.assets: list[Asset] produces PersonAssets, while Person.tags: list[Tag] would produce PersonTags.
Custom Junction Model¶
To add extra fields to the junction table, define the through model explicitly using ManyReferenceField:
from amsdal.models import Model, ManyReferenceField, ReferenceField
class Asset(Model):
name: str
class PersonAsset(Model):
person: 'Person' = ReferenceField(db_field='person_id')
asset: Asset = ReferenceField(db_field='asset_id')
is_favorite: bool = False
class Person(Model):
first_name: str
last_name: str
assets: list[Asset] = ManyReferenceField(
through=PersonAsset,
through_fields=('person', 'asset'),
)
through— the junction model classthrough_fields— a tuple of field names in the junction model: (field pointing to this model, field pointing to the related model)
Reverse M2M accessor¶
Pass related_name to opt into a target-side reverse accessor that traverses the through table:
class Person(Model):
assets: list[Asset] = ManyReferenceField(
through=PersonAsset,
through_fields=('person', 'asset'),
related_name='owners', # → asset.owners returns persons via PersonAsset
)
Default is None — no target-side reverse accessor is installed.
Table Configuration¶
Control how your model maps to the database using class-level attributes.
Custom Table Name¶
from typing import ClassVar
from amsdal.models import Model
class Person(Model):
__table_name__: ClassVar[str] = 'people'
first_name: str
last_name: str
Generates CREATE TABLE people (...) instead of CREATE TABLE Person (...).
Custom Primary Key¶
By default, AMSDAL adds a partition_key field as the primary key. Override it with __primary_key__:
from typing import ClassVar
from amsdal.models import Model
class Person(Model):
__primary_key__: ClassVar[list[str]] = ['person_id']
person_id: int
first_name: str
last_name: str
Composite primary key:
from typing import ClassVar
from amsdal.models import Model
class Person(Model):
__primary_key__: ClassVar[list[str]] = ['first_name', 'last_name']
first_name: str
last_name: str
Indexes¶
from typing import ClassVar
from amsdal.models import Model, IndexInfo
class Person(Model):
__indexes__: ClassVar[list[IndexInfo]] = [
IndexInfo(field='first_name', name='idx_first_name'),
]
first_name: str
last_name: str
Unique Constraints¶
from typing import ClassVar
from amsdal.models import Model, UniqueConstraint
class Person(Model):
__constraints__: ClassVar[list[UniqueConstraint]] = [
UniqueConstraint(fields=['first_name', 'last_name'], name='unique_full_name'),
]
first_name: str
last_name: str
On save in state mode (the default), uniqueness is enforced by the database engine itself; a duplicate raises AmsdalUniquenessError. In lakehouse-only mode the framework checks uniqueness in Python before writing, since the lakehouse table has no DB-level UNIQUE constraint.
Inheritance¶
Models support standard Python class inheritance. The child model gets all parent fields plus its own — they're merged into the child table at class build time (single-table layout). There is no automatic inter-table FK between child and parent.
from amsdal.models import Model
class Person(Model):
first_name: str
last_name: str
class Employee(Person):
company_name: str
Generated schema:
CREATE TABLE Person (
partition_key TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
PRIMARY KEY (partition_key)
)
CREATE TABLE Employee (
partition_key TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
company_name TEXT,
PRIMARY KEY (partition_key)
)
Each subclass becomes a standalone table. Person and Employee rows are completely independent — saving an Employee does not create or link to a Person row.
Validators¶
Options Validator¶
Restrict a field to a predefined set of values:
from typing import Any
from pydantic.functional_validators import field_validator
from amsdal.models import Model
from amsdal.models import validate_options
class Person(Model):
first_name: str
last_name: str
gender: str
@field_validator('gender')
@classmethod
def validate_gender(cls: type, value: Any) -> Any:
return validate_options(
value,
options=['Male', 'Female'],
)
Using validate_options keeps the allowed set declarative and runtime-checked while remaining a plain str field (unlike Literal[...], which fixes the type at compile time).
Non-Empty Key Dictionary Validator¶
Ensure all keys in a dictionary field are non-empty strings:
from typing import Any
from pydantic.functional_validators import field_validator
from amsdal.models import Model
from amsdal.models import validate_non_empty_keys
class Person(Model):
first_name: str
last_name: str
equipment: dict[str, str]
@field_validator('equipment')
@classmethod
def validate_equipment_keys(cls: type, value: Any) -> Any:
return validate_non_empty_keys(value)