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
59
60
61
62
63
64
65
66
67
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
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
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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.null 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
127
128
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
130
131
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
133
134
def get_constraints(self) -> typing.Any:
    return []

get_columns

get_columns(name)
Source code in saffier/core/db/fields/base.py
136
137
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
139
140
141
142
143
144
145
146
147
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
149
150
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
152
153
154
155
156
157
158
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
160
161
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
182
183
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
185
186
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
188
189
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
191
192
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
194
195
196
197
198
199
200
201
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
203
204
205
206
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
208
209
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
211
212
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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)