Subclass sustained.Model to describe a table. The subclass declares the table’s name, its columns, its relations, and the dialect and connection its queries use.
Guide: Models.
Class attributes you declare
| Attribute | Type | Default | Meaning |
|---|---|---|---|
tableName |
str |
None |
The table name. Required for column access, DDL, and queries. |
tableSchema |
str |
None |
Schema name, the middle segment of a qualified name. |
database |
str |
None |
Database name, the first segment of a qualified name. |
tableColumns |
dict[str, ColumnDef] |
None |
Typed column definitions. Drives create_table_sql() and migration generation. |
columns |
tuple[str, ...] |
None |
Declared column names. When set, access to any other name raises AttributeError. |
indexes |
list[Index] |
None |
Named indexes, created alongside the table. |
tableConstraints |
list[Check or ForeignKey] |
None |
Named table constraints, rendered into CREATE TABLE and kept in step by migration generation. |
tableOptions |
TableOptions |
None |
Storage clauses. Athena only; every other dialect raises DialectError. |
relationMappings |
dict[str, RelationMapping] |
{} |
Relations, keyed by the name you join or fetch by. |
Declaring tableColumns sets columns from the same keys, unless the class sets columns itself.
from sustained import Model
from sustained.schema import Integer, String
class Venue(Model):
tableName = 'venues'
tableColumns = {
'id': Integer(primary_key=True, autoincrement=True),
'name': String(120, nullable=False),
}
Venue.name # 'venues.name'
Venue.nmae # AttributeError, listing the declared columns
Column access
| Form | Returns | Notes |
|---|---|---|
Model.column |
str |
The fully qualified name: database.schema.table.column, skipping the parts the model does not set. |
instance.column |
the row value | An attribute set during hydration from the selected columns. A name the query did not select raises AttributeError. |
Model.c.column |
ColumnExpr |
A typed reference. Python operators on a ColumnExpr build Predicate objects. |
Model.column and Model.c.column raise AttributeError when tableName is unset, when the name starts with _, or when the model declares columns and the name is not one of them.
Queries and dialect
Model.query() -> QueryBuilder
A new builder on the model’s table, using the model’s dialect.
Model.set_dialect(dialect)
Sets the dialect for every query, DDL statement, and migration built from this class. Call it on Model to cover every model, or on a subclass to scope it to that subclass.
Connections
Model.bind(connection)
Attaches a DB-API 2.0 connection or a ConnectionPool. Binding on Model shares the connection with every model; binding on a subclass scopes it to that subclass.
Model.unbind()
Removes the binding.
Model.bind_async(adapter)
Attaches an AsyncAdapter. Model.unbind_async() removes it again.
Model.transaction(connection=None)
A context that commits on success and rolls back on any exception. Nested blocks use savepoints, spelled per dialect; on DuckDB, which has none, a nested block raises DialectError. Raises RuntimeError when no connection resolves.
Model.async_transaction(adapter=None)
The async equivalent. Nested blocks use savepoints, spelled the way the model’s dialect spells them.
The connection’s parameter style must match the dialect’s placeholder. See Dialect support.
DDL
Model.create_table_sql(if_not_exists=False) -> str
The CREATE TABLE statement from tableColumns and tableOptions. Raises ValueError when the model sets no tableColumns or no tableName.
Model.create_indexes_sql() -> list[str]
One CREATE INDEX statement per entry in indexes. Empty when indexes is unset.
Model.create_table_statements(if_not_exists=False) -> list[str]
The CREATE TABLE statement plus the CREATE INDEX statements.
Model.create_table(connection=None, if_not_exists=False)
Executes the CREATE TABLE and CREATE INDEX statements.
Model.drop_table_sql(if_exists=True) -> str
The DROP TABLE statement. Raises ValueError when the model sets no tableName.
Model.drop_table(connection=None, if_exists=True)
Executes the DROP TABLE statement.
Column types render per dialect. See Schema types.
Instances
run() and first() return instances with one attribute per result column. Instances do not lazy load, do not track changes, and have no save() method.
Model(**kwargs)
Sets each keyword as an attribute.
repr(instance)
ClassName(key=value, ...) over the instance’s attributes.
Relations
A relationMappings entry has three keys.
| Key | Value |
|---|---|
relation |
A RelationType member. |
modelClass |
The related model class, or its class name as a string. |
join |
How the two tables connect. |
The RelationType members are BelongsToOneRelation, HasManyRelation, HasOneRelation, and ManyToManyRelation.
A direct join mapping is {'from': 'a.id', 'to': 'b.a_id'}. Qualify both sides as table.column.
A through mapping adds the link table:
'join': {
'from': 'artists.id',
'through': {
'from': {'table': 'show_artists', 'key': 'artist_id'},
'to': {'table': 'show_artists', 'key': 'show_id'},
},
'to': 'shows.id',
}
String names in modelClass resolve through the model registry: every subclass with a tableName registers itself under its class name when the class is defined. Import the related class before you build the query, or resolution raises ValueError.
Registry functions
These live in sustained.model.
get_registered_model(name) -> type[Model] | None
Looks up a model by class name. Raises ValueError when two model classes share the name, because the registry keeps neither of them under it. The registry never drops an entry, so a name stays taken for the life of the process.
resolve_model_reference(reference, context_module=None) -> type[Model]
Returns a class unchanged, or resolves a string through the registry, then through context_module. Raises ValueError when neither one finds the class. A name two model classes share resolves through context_module only, and raises when that module does not define it.
Building a model at runtime
create_model(name, table_name, mappings=None, table_schema=None, database=None, columns=None)
create_model returns a new Model subclass. mappings becomes relationMappings, and columns sets the strict column tuple. The result behaves like a class you wrote by hand, and registers itself the same way.
from sustained import create_model, RelationType
Venue = create_model('Venue', 'venues')
Show = create_model(
'Show', 'shows',
mappings={
'venue': {
'relation': RelationType.BelongsToOneRelation,
'modelClass': Venue,
'join': {'from': 'shows.venue_id', 'to': 'venues.id'},
},
},
)