-
-
Notifications
You must be signed in to change notification settings - Fork 666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Fully merge Pydantic Field
with SQLAlchemy Column
constructor
#436
Conversation
📝 Docs preview for commit c93b42e at: https://6315c6b9c76bbe33cb57fcf6--sqlmodel.netlify.app |
Codecov ReportBase: 98.49% // Head: 97.90% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## main #436 +/- ##
==========================================
- Coverage 98.49% 97.90% -0.60%
==========================================
Files 185 188 +3
Lines 5856 6302 +446
==========================================
+ Hits 5768 6170 +402
- Misses 88 132 +44
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
c93b42e
to
6cb741b
Compare
📝 Docs preview for commit 6cb741b at: https://6315c7ab892a153d236388e6--sqlmodel.netlify.app |
📝 Docs preview for commit bbdea72 at: https://6315d1e5892a1545e9638201--sqlmodel.netlify.app |
It would be nice if class AnotherModel(SQLModel, table=True):
foo: int = Field(foreign_key='mymodel.custom_field', ondelete='CASCADE', onupdate='CASCADE') |
@nuno-andre I appreciate your suggestion, but I don't think this is a good idea for two reasons: 1) PEP 20The only reason I didn't remove the However, my actual intent with this PR is to mirror Pydantic's Field(ForeignKey("table.key", ondelete="CASCADE", onupdate="CASCADE"), ...) If we had the two parameters you proposed in addition, we would have yet another way to do the same thing. As I argued in my response to @amisadmin above:
I would argue that this obvious way is there now, and that it is not a big ask (or a lot of added verbosity) to pass a 2) Inconsistency & partially meaningless parametersThe Conversely, a In conclusion, although I recognize the benefit of having those dedicated parameters, I think the drawbacks outweigh this benefit significantly and therefore I don't think these parameters should be included. If you want to pursue this further, just like I suggested in the other discussion above, maybe you'll want to offer your own PR, when/if this is merged, and @tiangolo may agree with you and disagree with my reasoning. I hope this makes sense. Thanks for the feedback though. |
This is amazing work. Are there any plans on reviewing/merging it, @tiangolo ? Or do we need to provide something extra to make sure that these changes are valid and ready to be merged? |
bbdea72
to
cdd51ec
Compare
📝 Docs preview for commit cdd51ec at: https://63b58ab3facf617cfb46ea71--sqlmodel.netlify.app |
allow passing all `Column` arguments directly to `Field`; make `default` a keyword-only argument for `Field`
cdd51ec
to
0c0c3aa
Compare
📝 Docs preview for commit 0c0c3aa at: https://63b58bf2da28e97fcbe76f09--sqlmodel.netlify.app |
For what it's worth, I just rebased this branch once more on top of the most recent I also gave the current state of PRs and issues a cursory review and can add the following to the lists I started above. Affected issues (continued)
Affected PRs (continued)Would love to get feedback on this soon. I am still willing to continue working on this. |
@tiangolo Is there anything missing in order to have this PR merged? It looks like an amazing work has been put here and could give a lot of value to the community as this truly affect lots of PRs and open issues SQLModel currently have. |
Great work and really waiting for this to be merged! |
@daniil-berg In my opinion, the goal of SQLModel is not to replicate SQLAlchemy with a Pydantic syntax, but to add persistence to Pydantic models. Under this point of view, the classes assigned to attributes should signify the attribute typology (as subclasses of Regarding the inconsistency of including parameters not applicable in all cases, this is something that the Pydantic's In any case, this PR seems to me an amazing work that certainly deserves to be merged. |
Thank you @daniil-berg for the thorough explanation/argument and all the work! 🤓 🍰 And thanks everyone for the discussion here. ☕ I actually prefer not to fully merge the signatures of both So I have intentionally simplified and limited the things I support in SQLModel, to work for the majority of use cases (say 90%) in a very simple and intuitive way (at least I hope so) without all the more advanced/exotic features that SQLAlchemy supports. And then for those advanced use cases, there's a way to escape and use all the features from SQLAlchemy, defining the full column, all the arguments, etc. I see there are problems this PR would help with, here's my point of view on each of them:
Thanks again for the work and all the help with the rest of the project! 🍰 |
Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs. |
Abstract
The proposed changes modify the
Field
constructor to take all parameters defined on the SQLAlchemyColumn
directly, instead of just a few.This makes the parameters
sa_column
,sa_column_args
, andsa_column_kwargs
completely obsolete!There is no negative impact on current functionality. All test cases still pass without me modifying them. Test coverage is also not reduced.
Rationale
The definition of model fields should be fully in line with the goals of SQLModel, as stated in the documentation.
Intuitive to write
People coming from an SQLAlchemy background will immediately feel at home with the
Field
constructor, just as people from a Pydantic background already see all the familiar features of it. This honors the project's intent:Until now, leveraging all
Column
features was a little cumbersome and unclear, as evidenced by numerous issues/questions (see below) for trying to figure out how to design all but the most simple database models. Thesa_column
-parameters did allow all that functionality, but sometimes led to unexpected behavior for many people.Easy to use
Here are just a few examples of how these changes would make SQLModel easier to use.
Defining custom SQL types until now required initializing and passing a
Column
instance tosa_column
. This led to almost all database-relatedField
arguments to be ignored. This is how you had to do it:Whereas now, you can now write this:
Adding additional constraints, defining a different DB column name, etc. all required using
sa_column_args
/sa_column_kwargs
like this:Now you just need to do this:
Compatible
The package remains just as compatible with FastAPI, Pydantic and SQLAlchemy as before. If anything, allowing to pass all column parameters directly makes that compatibility even more obvious to people coming from SQLAlchemy to SQLModel.
Extensible/Short
see above
Implementation
Details (click me)
The changes cover basically only three things: The
FieldInfo
class, theField
function, and theget_column_from_field
function. Here are the broad points:class FieldInfo
FieldInfo
class (inheriting from the Pydantic one) has been extended to incorporate all the column parameters that have been missing.__slots__
were defined mimicking the Pydantic approach.__init__
method has been streamlined, but a new helper methodget_defined_column_kwargs
was added which comes into play later to construct a correspondingColumn
.def Field
FieldInfo
.DeprecationWarning
s have been added for thesa_column
-parameters.default
function parameter has been made keyword-only (like all the rest) and placed behind the newly added variable-length positional arguments*args
. (This is technically the only backwards incompatible change.)def get_column_from_field
*args
and**kwargs
for theColumn
based on the provided field as before, but supporting all possible parameters now.Affected issues (updated 2023-01-04)
sqlalchemy.Column
parameters are not passed forward when set onsqlmodel.Field
and a column is provided viasa_column
#314 - fixedget_column_from_field
|get_sqlalchemy_type
function behavior #503 - made irrelevantA major anticipated benefit of the proposed changes is the reduction of questions about "how to do SQLAlchemy-thing XYZ".
Affected PRs (updated 2023-01-04)
type_
kwarg insa_column_kwargs
#158 - made obsoleteField(YourType(**your_kwargs))
, but this is debateablesa_column
-style parameters are dropped/deprecated, as I would suggestField()
withsa_type
#505 - made obsoleteIt is obviously not my intention to trash other people's work. I respect the effort they put in, just think that those changes are no longer needed/sensible in conjunction with this PR.
Caveats & arguments against this
Details (click me)
Huge function signature
The
Field
function's signature will be even larger and perhaps overwhelming for newcomers.My response would be that a large function signature is not an argument per se, and that all arguments remain optional, which allows the actual code to be as concise as possible.
Commitment to incorporate future additions
Changes/additions to the
Column
parameters in SQLAlchemy in the future would necessitate adjusting the SQLModelField
function accordingly. Although this is no different from how Pydantic changes already need to be propagated to the customField
constructor, going the route suggested in this PR would require keeping track of both base packages (Pydantic and SQLAlchemy) in the future, to ensure maximum compatibility/familiarity.I don't think this is such a bad thing, since SQLModel aims combine their functionality. If/when SQLAlchemy 2 drops, major changes (maybe even backwards incompatible) may be necessary anyway.
Possible future parameter conflicts
Parameter names for Pydantic's
Field
and SQLAlchemy'sColumn
might conflict in the future.This is hypothetical and it would not necessarily even cause problems, but if it does, a possible solution would be to distinguish parameters that are completely unrelated with prefixes like
sa_param
andpy_param
or something similar.Backwards incompatible
default
parameterThe
default
parameter for theField
function is now keyword-only. Until now you can call it like thisField("a_default_value")
; with the proposed changes, you will have to call it like thisField(default="a_default_value")
.This is technically the only backwards incompatible change in this PR. However I have a few arguments for this:
default
parameter in theField
function. This may somewhat make sense with the original Pydantic version ofField
because there you would normally assign the default value to your model field directly. However, I don't find it intuitive/appropriate at all in the context of SQLModel since models are intended to represent database tables and fields represent database columns. The default value for a field/column is just one of many parameters and should not be treated differently. I do concede however that this makes is slightly less familiar to people that come from a Pydantic background, because they may be used to using a positional default.Future To-Dos
Details (click me)
Field
as it is with SQLAlchemyColumn
. This should be fairly straight-forward.sa_
-style parameters in favor of full integration toRelationship
attributes. This may be more difficult; I haven't had the time to look into that.Column
configurations work the same with SQLModel fields.I hope you will find the time to consider this, @tiangolo. I am looking forward to discussions, counter-arguments, and proposed changes to this from other contributors and am willing to keep working on this.