-
-
Notifications
You must be signed in to change notification settings - Fork 246
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
MySQL Boolean autogenerate comparson to TINYINT #605
Comments
this is because compare_type does not detect a MySQL TINYINT column as the same as Boolean, so you will need to adjust these migrations manually or use a custom type comparator. there is then a second issue that when "changing" the type of the column, it does not drop the old constraint. unfortunately there is no good way to automate these things and I have no means of fixing this issue in the near future. |
this is not specifically "enums" but this is the same area of issue. |
I do not understand totally because logically when doing the upgrade there is a conversion to the SQL language based on the type of database, no? Starting from this idea, it is possible to compare what has been generated in SQL and the structure of the table (for example via a "show create table"). I am wrong ? |
MySQL has a datatype called "TINYINT". You can use this datatype directly in your schema, but it is not a SQL standard type; only MySQL has it. SQLAlchemy has a datatype called "Boolean" that generates any number of different datatypes on databases, depending on what capabilities they have. On SQL Server, it uses BIT; on Postgresql, BOOLEAN, on MySQL, TINYINT. The thing is you can use TINYINT / BIT / etc. without them corresponding to SQLAlchemy's Boolean. Alembic therefore does not, at the moment, make the assumption that if a metadata model has Boolean, and the target database has TINYINT, that this TINYINT is the same datatype. This is 100% a bug, because even if the user did use TINYINT(display_width=1), and then changed their column to Boolean, that should not produce a change in datatype. That part of this is not too difficult to fix but needs a comprehensive approach and tests throughout the library to fully release. However, with all of that, the situation gets much worse. SQLAlchemy's Boolean and Enuim datatypes also generate CHECK constraints, which is a decision I would never have made today if I could have seen these issues coming. There's no autogenerate support for CHECK constraints either, and this would be very difficult to do generically. However, again in the case of Boolean / Enum, we should be taking a bit of a guess that there likely is a CHECK constraint on the table when these datatypes are used. However, that is also quite complicated because these CHECK constraints have no particular name (they are unnamed by default and as you can see MySQL just generates a name), and we'd have to basically search for them and parse their SQL text on a per-database basis in order to identify the ones that we think are relevant to our particular database column. So we can solve your immediate issue here with the first part of this but there's a longer term problem in locating and finding these CHECK constraints, such as if you changed your database type from Boolean to INTEGER, we'd need to DROP that constraint. |
I understand the difficulty now, thank you very much for the explanation. The best solution (for now) would be to disable the creation of constraints when desired. I tested it on my fields: paid = db.Column(db.Boolean, create_constraint=False)
satisfaction = db.Column(db.Boolean, create_constraint=False) But this does not work currently in alembic, the constraints are still created :( /../sqlalchemy/sql/base.py:299: SAWarning: Can't validate argument 'create_constraint'; can't locate any SQLAlchemy dialect named 'create'
% (k, dialect_name) |
that's an argument of boolean:
|
Sorry, i am tired 😅 , it works with correct initialization: paid = db.Column(db.Boolean(create_constraint=False))
satisfaction = db.Column(db.Boolean(create_constraint=False)) |
Faster than me :) thanks anyway ! |
I have been looking at this a bit. It kind of feels like the issue is with the I was thinking could move a bit of logic out to the type system- instead of Does it sound like that is a useful path to explore, or am I thinking about this all wrong? |
I think it's a valid idea to explore having Alembic use the type adaptation system to do the compare, but as far as the "move type_api.adapt_type" part I think you need to look at the bigger dialect-level part of this that should already do what you're looking for, which would be the dialect.type_descriptor() method - that gives you the type which the dialect uses, given a generic type. however, this won't work for the types we care about right now, e.g. boolean and enum, because there is no adaption used with these, the way Boolean becomes TINYINT is the MySQL compiler renders "BOOL" for it, and the way Enum becomes CHAR is that it just renders CHAR. were you proposing a change on the SQLAlchemy side? |
Yeah, my thought was to have the generic type come out as |
I think we'd have to rethink the approach on the SQLAlchemy side, since adaptation is only used to a limited extent, not really for DDL. A different architecture might not be a bad thing but I dont know if I have the appetite for that right now as that would be a huge change. The type comparison on the Alembic side is not really the biggest problem, a few hardcoded types is not the end of the world and it can be changed later. the bigger issue is the check constraint thing, and also the extra mechanics for Postgresql ENUMs. and all the tests for all of it. |
Okay- I'll work on the alembic only solution. Is there a way to introspect WHICH of those integer columns (TINYINT, BIT, etc) is used on a particular dialect, or do I just need to stick a switch statement of some sort (or an attribute on the dialect?) |
yeah...nothing too great, it's like visit_BOOLEAN -> BOOL in code like this: https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/dialects/mysql/base.py#L2057 there's a class called TypeCompiler that delivers the string representation of the type for each dialect. For Enum->CHAR it's in the base, because there is no enum type: https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/sql/compiler.py#L3398 then for mysql it switches at https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/dialects/mysql/base.py#L2018 . for table reflection, there's this whole "let's render the DDL for a type" but in reverse, it goes back to the more specific type, e.g. in this mapping: https://github.com/sqlalchemy/sqlalchemy/blob/master/lib/sqlalchemy/dialects/mysql/base.py#L1145 . this is what table reflection uses to get back the reflected types, in the case of MySQL BOOL we get TINYINT. so you can see the disconnect, the python-> DDL and DDL->python mappings are not symmetrical in special cases like this. this particular version of the typing system came along in version 0.6 or so of sqlalchemy. prior to that it might have worked in more of a class-hierarchy / class-adaptation system kind of way, the TypeCompiler thing came in to provide more fluidity to the system and to get rid of lots of extra classes and such. So there is, in fact, a way you could automate the comparison here. you could run the TypeCompiler and compare the strings:
here it is doing a tricky one for Postgresql:
still, the mysql impl needs to know that BOOL and TINYINT are synonymous:
I would probably have a short translation dicttionary in the alembic MySQL impl class for things like "BOOL" -> "TINYINT". this would also be a way that the type comparison could pick up on really small things like changes in length and things like that. i haven't had breakfast yet and wondering if I'm overlooking something really obviously not-working about this, because it seems like it might solve most of the problem |
Was looking at the
And then in MySqlImpl-
That code gets the simple tests I added passing, but... not familiar enough with the code base to know if that's a good or bad way of sticking that logic in there haha. Will also have to go through and look at the various database implementations to flesh out that mapping, but again wanted to run that through. I also noticed that the current implementation does not show changes between TINYINT() and TINYINT(display_width=1) - is this intentional, or should this be something that we are hoping to update as well? Last question- I was thinking adding one more step there- check if ANY check constraint(1, 0) exists on that column- if it doesn't then return True so that it gets added. Would involve a more introspection queries though. |
so sure, that looks like a start where we just have some rules for these specific types, that is fine. But I wonder if making a more dramatic leap and just comparing all types on the string representation like in my example above returned by the dialect would solve everything at once ? as far as comparing the elements inside the types, yes that is also something that "doesn't work" etiher right now. the string comparison approach would likely help with that also but might also produce false positives. |
I put in a first draft PR for this. As you guessed, using the type compiler gets you 90% of the way there- the only things left to handle are default precisions and the couple weird cases we already mentioned. All feedback welcome (am going to look at the constraint issue as well - have some ideas) |
Paul Becotte has proposed a fix for this issue in the master branch: Update the type comparison code used for schema autogeneration. Compare https://gerrit.sqlalchemy.org/1561 |
split out the CHECK part of this, which I'm fairly pessimistic anything is going to really happen for a long time, into #652. |
Paul Becotte has proposed a fix for this issue in the master branch: Update the type comparison code used for schema autogeneration. Compare https://gerrit.sqlalchemy.org/1561 |
Alembic version: 1.2.1
SQL: MariaDB
Each upgrade (with compare_type enabled) duplicate constraints :
The text was updated successfully, but these errors were encountered: