Open xlorepdarkhelm opened 2 months ago
@xlorepdarkhelm given the amount of code in this file, does it make sense to split this work apart a bit and focus on smaller sections at a time?
I'm also assuming that the acceptance criteria ought to be something like this:
Does that look right to you? Is there anything else you can think of? Thanks!
That seems like goot acceptance criteria.
And I can see this being split up. I don't believe that there would be any issue with only part of the models being converted. In theory, they can work side-by-side.
Yeah, I think it'd have to be split up, there's too much otherwise. This may become its own epic at this level then and we can divide and conquer with the issues that roll-up into it. If you think you have a good breakdown of which groups of models to focus on, let's list them out here and plan from there. 🙂
To migrate the code to SQLAlchemy 2.0 style using Flask-SQLAlchemy, several changes that align with the updated APIs and best practices of SQLAlchemy 2.0 are needed. Below are the key changes you should consider for each part of the code:
Mapped
and Type HintsSQLAlchemy 2.0 encourages the use of type hints to clearly define column types and relationships. This involves replacing db.Column
, db.relationship
, and other constructs with appropriate type hints.
Example:
from sqlalchemy.orm import Mapped, mapped_column, relationship
class User(db.Model):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(db.String, nullable=False, index=True)
email_address: Mapped[str] = mapped_column(db.String(255), nullable=False, index=True, unique=True)
# Other fields and relationships...
services: Mapped[list["Service"]] = relationship("Service", secondary="user_to_service", back_populates="users")
organizations: Mapped[list["Organization"]] = relationship("Organization", secondary="user_to_organization", back_populates="users")
Replace db.relationship
and backref
with explicit relationship
and back_populates
for better clarity.
Example:
class Service(db.Model):
__tablename__ = "services"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(db.String(255), nullable=False, unique=True)
created_by_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), db.ForeignKey("users.id"), nullable=False)
created_by: Mapped["User"] = relationship("User", back_populates="services")
# Other fields and relationships...
And in the User
model:
class User(db.Model):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
services: Mapped[list["Service"]] = relationship("Service", back_populates="created_by")
Mapped
for CollectionsFor collections or lists of related items, use Mapped
with type hints.
Example:
class Service(db.Model):
__tablename__ = "services"
service_sms_senders: Mapped[list["ServiceSmsSender"]] = relationship("ServiceSmsSender", back_populates="service")
templates: Mapped[list["Template"]] = relationship("Template", back_populates="service")
Constraints and indexes don't change significantly, but ensure they are clearly defined.
Example:
__table_args__ = (
db.UniqueConstraint("user_id", "service_id", name="uix_user_to_service"),
)
Enum columns and other specialized columns should remain mostly the same but ensure they're well-defined with Mapped
.
Example:
auth_type: Mapped[AuthType] = mapped_column(
db.Enum(AuthType, name="auth_types"),
index=True,
nullable=False,
default=AuthType.SMS
)
SQLAlchemy 2.0 encourages clear class methods, so ensure all methods align with the 2.0 syntax.
Example:
@validates("mobile_number")
def validate_mobile_number(self, key: str, number: str) -> str:
try:
if number is not None:
return validate_phone_number(number, international=True)
except InvalidPhoneError as err:
raise ValueError(str(err)) from err
When dealing with sessions, SQLAlchemy 2.0 prefers direct usage of Session
. Flask-SQLAlchemy abstracts this for you, so if you're using db.session
, you're generally fine.
Mapped
and type hints to fields and relationships.db.relationship
with relationship
and use back_populates
instead of backref
.By systematically applying these changes, you'll gradually upgrade your models to SQLAlchemy 2.0 style while maintaining compatibility with Flask-SQLAlchemy.
User
will impact several related models.Service
and EmailBranding
.User
and Organization
.User
and Service
.User
and Service
are updated.Service
and important for service functionality.Service
, which should be updated first.Service
.Template
.Job
, Template
, and Service
.Organization
and Service
.Organization
.User
and Organization
.User
for handling WebAuthn credentials.Notification
and Service
.Service
.The logical progression is to start with foundational models (User
, Organization
), followed by primary functional models (Service
, ServiceUser
, ServicePermission
), then move on to secondary functionality (Template
, ApiKey
, Job
). After that, update the notification system, followed by administrative models and finally ancillary and reporting models.
I made the separate issues (above) for the breakdown for which models to do. Due to the nature of this kind of change, these need to be done in synchronous order, as many of them are relying on the previous step(s) to be completed. I have noted each as such in their descriptions.
Awesome, thank you for this write-up @xlorepdarkhelm, including the examples and summary! Thank you for splitting the issues apart as well. Given that, I'm going to convert this issue as the parent to an epic and then we'll pull the other issues in to the board as things get prioritized/planned for with future sprints.
The only thing I'd amend in these is making sure the issues also have a Security Considerations
section in them after the Acceptance Criteria
. I imagine in most cases this is lightweight as there likely aren't any, but we should be explicit about that regardless and make sure we're thinking things through. 🙂
The only thing I'd amend in these is making sure the issues also have a
Security Considerations
section in them after theAcceptance Criteria
. I imagine in most cases this is lightweight as there likely aren't any, but we should be explicit about that regardless and make sure we're thinking things through. 🙂
I added security considerations to each of the sub-issues for this epic.
Now that we are using SQLAlchemy 2.0, we should begin the process of moving away from the deprecated SQLAlchemy 1.x method of doing things, and moving into the new SQLAlchemy 2.x form. To start, the
models.py
file needs some updating.The first change for this is rewriting all of the declared columns in the models from the old syntax of
And change it to using the new mapper syntax of:
The important thing is that the type is now handled in the
Mapped[]
type definition, and thatdb.Column
is replaced withmapped_column
. This will bring the models up to SQLAlchemy 2.0 standards, and should be a direct conversion with no expected issues.Note:
flask-sqlalchemy
should handle getting thedb.Model
configured correctly for this new syntax.Note 2: SQLAlchemy 2.0 prefers using the standard Python types (
str
,int
, etc) over using the SQLAlchemy column type equivalents (db.String
,db.Integer
, etc).Ref: https://docs.sqlalchemy.org/en/20/orm/quickstart.html