- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 782
 
          ✨ Fully merge Pydantic Field with SQLAlchemy Column constructor
          #436
        
          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
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
Fieldconstructor to take all parameters defined on the SQLAlchemyColumndirectly, instead of just a few.This makes the parameters
sa_column,sa_column_args, andsa_column_kwargscompletely 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
Fieldconstructor, 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
Columnfeatures 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
Columninstance tosa_column. This led to almost all database-relatedFieldarguments 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_kwargslike 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
FieldInfoclass, theFieldfunction, and theget_column_from_fieldfunction. Here are the broad points:class FieldInfoFieldInfoclass (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_kwargswas added which comes into play later to construct a correspondingColumn.def FieldFieldInfo.DeprecationWarnings have been added for thesa_column-parameters.defaultfunction 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*argsand**kwargsfor theColumnbased on the provided field as before, but supporting all possible parameters now.Affected issues (updated 2023-01-04)
sqlalchemy.Columnparameters are not passed forward when set onsqlmodel.Fieldand a column is provided viasa_column#314 - fixedget_column_from_field|get_sqlalchemy_typefunction 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_columnonupdatetimestamps #372 - made obsolete, if thesa_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
Fieldfunction'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
Columnparameters in SQLAlchemy in the future would necessitate adjusting the SQLModelFieldfunction accordingly. Although this is no different from how Pydantic changes already need to be propagated to the customFieldconstructor, 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
Fieldand SQLAlchemy'sColumnmight 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_paramandpy_paramor something similar.Backwards incompatible
defaultparameterThe
defaultparameter for theFieldfunction 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:
defaultparameter in theFieldfunction. This may somewhat make sense with the original Pydantic version ofFieldbecause 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)
Fieldas it is with SQLAlchemyColumn. This should be fairly straight-forward.sa_-style parameters in favor of full integration toRelationshipattributes. This may be more difficult; I haven't had the time to look into that.Columnconfigurations 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.