2. Defining Tables
The syntax for creating a table is very different, but built on the same principles:
from pydal import Field
# pydal:
db.define_table('my_table', Field('my_field'))
from typedal import TypedTable, TypedField
# typedal:
@db.define()
class MyTable(TypedTable):
my_field: TypedField[str] # or just `my_field: str`
In this example, pydal's Field('my_field') implicitly sets the type to 'string'.
The TypeDAL variant uses Python type annotations and maps these to the right database types.
The TypedField[] annotation is not necessary (at runtime), but improves type hinting and IDE support.
Any keyword arguments you would pass to db.define_table, you can also pass to db.define().
All Types
| pydal | typedal (native python type) | typedal (using TypedField annotations) | typedal (using TypedField) | typedal (using specific Field) |
|---|---|---|---|---|
Field('name', 'string') |
name: str |
name: TypedField[str] |
name = TypedField(str) |
name = StringField() |
Field('name', 'string', requires=IS_IN_SET([...])) |
name: Literal["a", "b"] or name: MyEnum |
name: TypedField[Literal["a", "b"]] or name: TypedField[MyEnum] |
name = TypedField(Literal["a", "b"]) or name = TypedField(MyEnum) |
name = StringField(requires=IS_IN_SET([...])) |
Field('name', 'text') |
× | × | name = TypedField(str, type="text") |
name = TextField() |
Field('name', 'blob') |
name: bytes |
name: TypedField[bytes] |
name = TypedField(bytes) |
name = BlobField() |
Field('name', 'boolean') |
name: bool |
name: TypedField[bool] |
name = TypedField(bool) |
name = BooleanField() |
Field('name', 'integer') |
name: int |
name: TypedField[int] |
name = TypedField(int) |
name = IntegerField() |
Field('name', 'double') |
name: float |
name: TypedField[float] |
name = TypedField(float) |
name = DoubleField() |
Field('name', 'decimal(n,m)') |
name: decimal.Decimal |
name: TypedField[decimal.Decimal] |
name = TypedField(decimal.Decimal) |
name = DecimalField(n=n, m=m) |
Field('name', 'date') |
name: datetime.date |
name: TypedField[datetime.date] |
name = TypedField(datetime.date) |
name = DateField() |
Field('name', 'time') |
name: datetime.time |
name: TypedField[datetime.time] |
name = TypedField(datetime.time) |
name = TimeField() |
Field('name', 'datetime') |
name: datetime.datetime |
name: TypedField[datetime.datetime] |
name = TypedField(datetime.datetime) |
name = DatetimeField() |
Field('name', 'password') |
× | × | name = TypedField(str, type="password") |
name = PasswordField() |
Field('name', 'upload') |
× | × | name = TypedField(str, type="upload") |
name = UploadField() |
Field('name', 'reference <table>') |
name: Table |
name: TypedField[Table] |
name = TypedField(Table) |
name = ReferenceField('table') |
Field('name', 'list:string') |
name: list[str] |
name: TypedField[list[str]] |
name = TypedField(list[str]) |
name = ListStringField() |
Field('name', 'list:integer') |
name: list[int] |
name: TypedField[list[int]] |
name = TypedField() |
name = ListIntegerField() |
Field('name', 'list:reference <table>') |
name: list[Table] |
name: TypedField[list[Table]] |
name = TypedField() |
name = ListReferenceField('table') |
Field('name', 'json') |
× | × | name = TypedField() |
name = JSONField() |
Field('name', 'bigint') |
× | × | name = TypedField() |
name = BigintField() |
Field('name', 'big-id') |
× | × | × | × |
Field('name', 'big-reference') |
× | × | × | × |
Enum fields
TypeDAL supports enum.Enum subclasses directly (including enum.StrEnum and enum.IntEnum):
import enum
class Status(enum.StrEnum):
DRAFT = "draft"
PUBLISHED = "published"
class Priority(enum.IntEnum):
LOW = 1
HIGH = 2
@db.define()
class Article(TypedTable):
status: Status
priority: Priority
Important constraints and behavior:
- All enum member values in one enum must share the same underlying Python type for DB fields.
Mixed enums (for example
str+intin one enum class) raiseTypeErrorwhen defining the table. - Reading rows with invalid enum values in the database does not crash. Those values are returned as
typedal.enum_helpers.InvalidEnumValue, where.valueisNone.
Making a field required/optional
| pydal | typedal (native python type) | typedal (using TypedField annotation) | typedal (using TypedField) | typedal (using specific Field) |
|---|---|---|---|---|
Field('name', 'string', required=True) |
name: str |
name: TypedField[str] |
name = TypedField(str, required=True) |
name = StringField(required=True) |
Field('name', 'text', required=False) |
name: typing.Optional[str] or name: str | None |
name: TypedField[typing.Optional[str]] or name: TypedField[str | None] |
name = TypedField(str, type="text", required=False) |
name = StringField(required=False) |
Hooks
Some logic can be added when data is added/edited/deleted from the database. This can be done just as web2py does ( see their docs)
from typedal import TypedTable
from typedal.types import OpRow, Reference, Set
class MyTable(TypedTable):
...
def my_before_insert(row: MyTable):
"""`row` to be inserted, can still be edited."""
# return True to cancel
def my_after_insert(row: MyTable, idx: Reference):
"""`row` that was just inserted with the new row id(x)."""
MyTable.before_insert(my_before_insert)
MyTable.after_insert(my_after_insert)
row = MyTable.insert(...) # to trigger
def my_before_update(query: Set, changes: OpRow):
"""`changes` to be applied to the row selection Set, can still be edited"""
# return True to cancel
def my_after_update(query: Set, changes: OpRow):
"""`changes` that were applied to the row selection Set"""
MyTable.before_update(my_before_update)
MyTable.after_update(my_after_update)
row.update_record(...) # to trigger
MyTable.where(...).update(...) # to trigger
def my_before_delete(query: Set):
"""rows matching `query` will be deleted"""
# return True to cancel
def my_after_delete(query: Set):
"""Selecting `query` should now yield no results"""
row.delete_record() # to trigger
MyTable.where(...).delete() # to trigger
Now that we have some tables, it's time to actually query them! Let's go to 3. Building Queries to learn how.