Skip to content

DB Schema Upsertion

Warning

As with many of the modules presented here, better solutions exist elsewhere if you have the time to devote to learn and use them. Here Alembic is the way to go to properly handle rigorous (and version controlled) database schema migration. The alternative that we offer here has only for merit its simplicity and ease of use.

Database migration is always a touchy and tricky topic.

The solution we present here only do two things:

  • Update existing EnumType types
  • Add new columns to existing tables

But it does it automatically, so that one never has to worry about writing any migration script. The obvious downside being that the migration are not version controlled, but for product in their infancy it is a perfectly fine tradeoff in our experience

Update existing EnumType types

the method add_missing_enum_values allows one to update EnumType already present in database with new values added in the code, and does so automatically by introspection.

What this means if that if one starts with the Permission enum

@unique
class Permission(str, Enum):
    """
    Enum listing all permission levels an application user can have
    """
    ADMIN = 'Admin'
    Consultant = 'Consultant'

like so, associated to a column of an existing table, and then decide to add a new value to the enum

@unique
class Permission(str, Enum):
    """
    Enum listing all permission levels an application user can have
    """
    ADMIN = 'Admin'
    Consultant = 'Consultant'
    Client = 'Client'

Then calling

add_missing_enum_values(Permission, session)

will automatically add the new Client value on the database side. We recommend on our side to create a list of enums to update

# List of all enums for which new values in the associated enum class are automatically added
# on the db side
ENUMS_TO_MIGRATE: list = [
]

and to call at FastAPI/Dash app startup

log.info('inserting new enum data')
for enum_name in ENUMS_TO_MIGRATE:
    add_missing_enum_values(enum_name, session)

Add new columns to existing tables

the method add_missing_columns allows one to update an existing SQLModel class (with table=True): adding a new column on the code side automatically adds it on the database side at the method call.

What this means if that if one starts with the UpFoo table

class UpFoo(SQLModel, table=True):  # type: ignore
    """
    Test class to tes db upsertion
    """
    __tablename__ = 'up_foo'
    id: Optional[int] = Field(default=None, primary_key=True)
    bar1: str = sfield()
    bar2: bool = sfield()
    bar3: str = field()
    bar4: bool = field()
    bar5: float = field()
    bar6: int = field()
    bar7: datetime = field()
    bar8: Permission = field()
    bar9: Optional[str] = field(default=None)

and then add new columns

class UpFoo(SQLModel, table=True):  # type: ignore
    """
    Test class to tes db upsertion
    """
    __tablename__ = 'up_foo'
    id: Optional[int] = Field(default=None, primary_key=True)
    bar1: str = sfield()
    bar2: bool = sfield()
    bar3: str = field()
    bar4: bool = field()
    bar5: float = field()
    bar6: int = field()
    bar7: datetime = field()
    bar8: Permission = field()
    bar9: Optional[str] = field(default=None)
    bar10: Optional[int] = Field(default=None)
    bar100: int = Field(default=0)
    bar11: float | None = Field(default=None)
    bar110: float = Field(default=0.0)
    bar12: str = Field(default='toto')
    bar120: Optional[str] = Field(default=None)
    bar13: Permission = Field(default=Permission.Client)
    bar130: Optional[Permission] = Field(default=None)
    bar14: Optional[int] = Field(default=None, index=True, foreign_key='app_user.id')
    bar15: bool = Field(default=True)
    bar150: Optional[bool] = Field(default=None)
    bar16: bytes = Field(default=b'toto')
    bar160: Optional[bytes] = Field(default=None)
    bar17: dict = Field(sa_type=JSONB, default={'key': 'toto'})
    bar170: Optional[dict] = Field(sa_type=JSONB)

Then calling

add_missing_enum_values(UpFoo, session)

will automatically add the new bar fields database side. We recommend on our side to create a list of classes to update

# List of all table for which new columns in the associated sqlmodel class are automatically added
# on the db side
TABLES_TO_UPDATE: list = [
]

and to call at FastAPI/Dash app startup

log.info('inserting new columns in existing tables')
for table_name in TABLES_TO_UPDATE:
    add_missing_columns(table_name, session)

Note

as of 2025/10/01 we cover the following python types (both allowing and forbidding NULL on the database side):

  • str
  • int
  • float
  • EnumType
  • bytes
  • bool
  • JSONB

To be noted, the code also allows to - add an index on the new column - associate the new column to another table via a foreign key

The code snippet above illustrates all of these features.