Skip to content

Field

All Saffier model fields inherit from Field.

The base class is where Saffier joins together three different concerns:

  • validation through an internal validator object
  • SQLAlchemy column generation
  • ORM-side normalization such as composite-field expansion or relation cleanup

Common options shared by most fields

The exact supported arguments vary by field type, but the base field behavior is where options such as these are enforced:

  • primary_key
  • null
  • default
  • server_default
  • index
  • unique
  • column_name
  • exclude
  • secret

Why the base field matters

When you define a custom field or debug an unexpected write behavior, the base field API is the contract to understand:

  • clean() converts a logical field value into one or more database columns
  • modify_input() can expand or rewrite incoming payloads
  • get_global_constraints() can add foreign keys or indexes to the table
  • pre_save_callback() can generate write-time values before persistence

saffier.core.db.fields.base.Field

Field(
    *,
    primary_key=False,
    index=False,
    unique=False,
    **kwargs,
)

Base class for Saffier model fields.

A field coordinates three layers of behavior: runtime validation through an internal validator, SQLAlchemy column generation, and ORM-specific input or relation normalization. Concrete subclasses usually override at least the validator or the column type, while relation-aware fields also override input normalization and pre-save behavior.

Source code in saffier/core/db/fields/base.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
    self,
    *,
    primary_key: bool = False,
    index: bool = False,
    unique: bool = False,
    **kwargs: typing.Any,
) -> None:
    self.server_default = kwargs.pop("server_default", None)
    if primary_key:
        default_value = kwargs.get("default")
        self.raise_for_non_default(default=default_value, server_default=self.server_default)
        kwargs["read_only"] = True

    self.null = kwargs.get("null", False)
    self.default_value = kwargs.get("default")
    self.primary_key = primary_key
    self.index = index
    self.unique = unique
    self.validator: SaffierField | type[SaffierField] = self.get_validator(**kwargs)
    self.comment = kwargs.get("comment")
    self.column_name = kwargs.pop("column_name", None)
    self.owner = kwargs.pop("owner", None)
    self.registry = kwargs.pop("registry", None)
    self.name = kwargs.pop("name", "")
    self.inherit = kwargs.pop("inherit", True)
    self.no_copy = kwargs.pop("no_copy", False)
    self.exclude = kwargs.pop("exclude", False)
    self.inject_default_on_partial_update = kwargs.get(
        "inject_default_on_partial_update",
        False,
    )
    self.server_onupdate = kwargs.pop("server_onupdate", None)
    self.autoincrement = kwargs.pop("autoincrement", False)
    self.secret = kwargs.pop("secret", False)

is_virtual class-attribute instance-attribute

is_virtual = False

server_default instance-attribute

server_default = pop('server_default', None)

null instance-attribute

null = get('null', False)

default_value instance-attribute

default_value = get('default')

primary_key instance-attribute

primary_key = primary_key

index instance-attribute

index = index

unique instance-attribute

unique = unique

validator instance-attribute

validator = get_validator(**kwargs)

comment instance-attribute

comment = get('comment')

column_name instance-attribute

column_name = pop('column_name', None)

owner instance-attribute

owner = pop('owner', None)

registry instance-attribute

registry = pop('registry', None)

name instance-attribute

name = pop('name', '')

inherit instance-attribute

inherit = pop('inherit', True)

no_copy instance-attribute

no_copy = pop('no_copy', False)

exclude instance-attribute

exclude = pop('exclude', False)

inject_default_on_partial_update instance-attribute

inject_default_on_partial_update = get(
    "inject_default_on_partial_update", False
)

server_onupdate instance-attribute

server_onupdate = pop('server_onupdate', None)

autoincrement instance-attribute

autoincrement = pop('autoincrement', False)

secret instance-attribute

secret = pop('secret', False)

get_column

get_column(name)

Build the SQLAlchemy column used to persist this field.

PARAMETER DESCRIPTION
name

Logical field name declared on the model.

TYPE: str

RETURNS DESCRIPTION
Column

sqlalchemy.Column: SQLAlchemy column configured from the field

Column

options and validator metadata.

Source code in saffier/core/db/fields/base.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def get_column(self, name: str) -> sqlalchemy.Column:
    """Build the SQLAlchemy column used to persist this field.

    Args:
        name: Logical field name declared on the model.

    Returns:
        sqlalchemy.Column: SQLAlchemy column configured from the field
        options and validator metadata.
    """
    column_type = self.get_column_type()
    constraints = self.get_constraints()
    column_kwargs = {
        "primary_key": self.primary_key,
        "nullable": self.get_columns_nullable() and not self.primary_key,
        "index": self.index,
        "unique": self.unique,
        "default": self.default_value,
        "comment": self.comment,
        "server_default": self.server_default,
    }
    if self.autoincrement:
        column_kwargs["autoincrement"] = True

    return sqlalchemy.Column(
        self.column_name or name,
        column_type,
        *constraints,
        key=name,
        **column_kwargs,
    )

get_validator

get_validator(**kwargs)
Source code in saffier/core/db/fields/base.py
136
137
def get_validator(self, **kwargs: typing.Any) -> SaffierField:
    return SaffierField(**kwargs)  # pragma: no cover

get_column_type

get_column_type()
Source code in saffier/core/db/fields/base.py
139
140
def get_column_type(self) -> sqlalchemy.types.TypeEngine:
    raise NotImplementedError()  # pragma: no cover

get_constraints

get_constraints()
Source code in saffier/core/db/fields/base.py
142
143
def get_constraints(self) -> typing.Any:
    return []

get_columns_nullable

get_columns_nullable()

Resolve SQLAlchemy nullability for this field during metadata build.

The model declaration remains the canonical field definition, but migration generation can temporarily ask Saffier to render selected columns as nullable. That temporary override makes it possible to add a required field to a table that already has rows, then backfill defaults in the generated online migration before the final model constraint is enforced by later migrations.

RETURNS DESCRIPTION
bool

True when the field declaration or active migration

TYPE: bool

bool

override should make generated columns nullable.

Source code in saffier/core/db/fields/base.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def get_columns_nullable(self) -> bool:
    """Resolve SQLAlchemy nullability for this field during metadata build.

    The model declaration remains the canonical field definition, but
    migration generation can temporarily ask Saffier to render selected
    columns as nullable. That temporary override makes it possible to add a
    required field to a table that already has rows, then backfill defaults
    in the generated online migration before the final model constraint is
    enforced by later migrations.

    Returns:
        bool: ``True`` when the field declaration or active migration
        override should make generated columns nullable.
    """
    if self.null:
        return True

    force_fields = FORCE_FIELDS_NULLABLE.get()
    owner_name = self.owner.__name__ if self.owner is not None else ""
    return (owner_name, self.name) in force_fields or ("", self.name) in force_fields

get_columns

get_columns(name)
Source code in saffier/core/db/fields/base.py
166
167
def get_columns(self, name: str) -> typing.Sequence[sqlalchemy.Column]:
    return [self.get_column(name)]

get_global_constraints

get_global_constraints(name, columns, *, schema=None)
Source code in saffier/core/db/fields/base.py
169
170
171
172
173
174
175
176
177
def get_global_constraints(
    self,
    name: str,
    columns: typing.Sequence[sqlalchemy.Column],
    *,
    schema: str | None = None,
) -> typing.Sequence[sqlalchemy.Constraint | sqlalchemy.Index]:
    del name, columns, schema
    return []

has_column

has_column()
Source code in saffier/core/db/fields/base.py
179
180
def has_column(self) -> bool:
    return not self.is_virtual

get_embedded_fields

get_embedded_fields(field_name, existing_fields)
Source code in saffier/core/db/fields/base.py
182
183
184
185
186
187
188
def get_embedded_fields(
    self,
    field_name: str,
    existing_fields: dict[str, "Field"],
) -> dict[str, "Field"]:
    del field_name, existing_fields
    return {}

expand_relationship

expand_relationship(value)
Source code in saffier/core/db/fields/base.py
190
191
def expand_relationship(self, value: typing.Any) -> typing.Any:
    return value

clean

clean(name, value, *, for_query=False)

Normalize one logical field value into column-value pairs.

PARAMETER DESCRIPTION
name

Logical field name.

TYPE: str

value

User-facing field value.

TYPE: Any

for_query

Reserved for field implementations that need distinct query-time normalization.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
dict[str, Any]

dict[str, Any]: Database payload keyed by column name.

Source code in saffier/core/db/fields/base.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def clean(
    self, name: str, value: typing.Any, *, for_query: bool = False
) -> dict[str, typing.Any]:
    """Normalize one logical field value into column-value pairs.

    Args:
        name: Logical field name.
        value: User-facing field value.
        for_query: Reserved for field implementations that need distinct
            query-time normalization.

    Returns:
        dict[str, Any]: Database payload keyed by column name.
    """
    del for_query
    if not self.has_column():
        return {}
    return {name: value}

modify_input

modify_input(name, kwargs)
Source code in saffier/core/db/fields/base.py
212
213
def modify_input(self, name: str, kwargs: dict[str, typing.Any]) -> None:
    del name, kwargs

raise_for_non_default

raise_for_non_default(default, server_default)
Source code in saffier/core/db/fields/base.py
215
216
def raise_for_non_default(self, default: typing.Any, server_default: typing.Any) -> typing.Any:
    del default, server_default

get_default_value

get_default_value()
Source code in saffier/core/db/fields/base.py
218
219
def get_default_value(self) -> typing.Any:
    return self.validator.get_default_value()

has_default

has_default()
Source code in saffier/core/db/fields/base.py
221
222
def has_default(self) -> bool:
    return self.validator.has_default()

get_default_values

get_default_values(field_name, cleaned_data)
Source code in saffier/core/db/fields/base.py
224
225
226
227
228
229
230
231
def get_default_values(
    self,
    field_name: str,
    cleaned_data: dict[str, typing.Any],
) -> dict[str, typing.Any]:
    if field_name in cleaned_data or not self.has_column():
        return {}
    return {field_name: self.get_default_value()}

is_required

is_required()
Source code in saffier/core/db/fields/base.py
233
234
235
236
def is_required(self) -> bool:
    if self.primary_key and self.autoincrement:
        return False
    return not (self.null or self.server_default is not None or self.has_default())

get_is_null_clause

get_is_null_clause(column)
Source code in saffier/core/db/fields/base.py
238
239
def get_is_null_clause(self, column: typing.Any) -> typing.Any:
    return column == None  # noqa: E711

get_is_empty_clause

get_is_empty_clause(column)
Source code in saffier/core/db/fields/base.py
241
242
def get_is_empty_clause(self, column: typing.Any) -> typing.Any:
    return self.get_is_null_clause(column)

operator_to_clause

operator_to_clause(field_name, operator, table, value)

Translate one lookup operator into a SQLAlchemy clause.

PARAMETER DESCRIPTION
field_name

Logical field name being filtered.

TYPE: str

operator

Saffier lookup suffix such as exact or isnull.

TYPE: str

table

SQLAlchemy table containing the target column.

TYPE: Table

value

Lookup value supplied by the caller.

TYPE: Any

RETURNS DESCRIPTION
Any

SQLAlchemy boolean clause implementing the lookup.

TYPE: Any

Source code in saffier/core/db/fields/base.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def operator_to_clause(
    self,
    field_name: str,
    operator: str,
    table: sqlalchemy.Table,
    value: typing.Any,
) -> typing.Any:
    """Translate one lookup operator into a SQLAlchemy clause.

    Args:
        field_name: Logical field name being filtered.
        operator: Saffier lookup suffix such as `exact` or `isnull`.
        table: SQLAlchemy table containing the target column.
        value: Lookup value supplied by the caller.

    Returns:
        Any: SQLAlchemy boolean clause implementing the lookup.
    """
    column = table.columns[field_name]
    mapped_operator = settings.filter_operators.get(operator, operator)

    if mapped_operator == "isnull":
        is_null = self.get_is_null_clause(column)
        return is_null if value else sqlalchemy.not_(is_null)

    if mapped_operator == "isempty":
        is_empty = self.get_is_empty_clause(column)
        return is_empty if value else sqlalchemy.not_(is_empty)

    return getattr(column, mapped_operator)(value)