Computed column support#1896
Conversation
| protected override string FormatNullable(ColumnDefinition column) | ||
| { | ||
| if (column.IsNullable == true && column.Type == null && !string.IsNullOrEmpty(column.CustomType)) | ||
| if (column.IsNullable == true && column.Type == null && !string.IsNullOrEmpty(column.CustomType) && column.Expression == null) |
There was a problem hiding this comment.
I really don't understand the purpose of this override to begin with. Only in the case of using AsCustom does it provide NULL for a nullable field. Why for AsCustom but not for any other data types? I can see that perhaps it was desirable to specify NULL or NOT NULL to override a non-default database option of ANSI_NULL_DFLT_OFF, but then why would it be only for AsCustom and no other data types? If this condition were removed, the entire overridden method could be removed.
There was a problem hiding this comment.
Most likely it is because for all the other types, there are C# structs that map to them and the default is to be non-null. You have to wrap a struct as Nullable where T is a value type.
I agree, it is weird but would be a breaking change. If we break it, it would ideally be done in a separate PR and we rebase this PR - this way the PR auto-generates in the release notes as a breaking change and is a lot clearer to end-users.
Is SqlServer2008Column or later re-overriding this behavior though?
There was a problem hiding this comment.
Is SqlServer2008Column or later re-overriding this behavior though?
No it isn't.
Use CompatibilityMode.HandleCompatibility |
| /// <param name="expression">The expression to calculate</param> | ||
| /// <param name="stored">Whether the computed column is virtual or stored</param> | ||
| /// <returns>The next step</returns> | ||
| TNext AsExpression(string expression, bool stored = false); |
There was a problem hiding this comment.
Hello, just my two cents, but I think this naming may confuse people, because of C# Expression trees for example, while it is dedicated to computed columns. Why not name it simply AsComputed(string expression, bool stored = false) ?
There was a problem hiding this comment.
Makes sense. Also it’s now treated as an option, so like Identity or Indexed, maybe it should be called Computed?
There was a problem hiding this comment.
@PhenX Good question and your feedback serves as a "Keeper of the Flame."
I originally suggested AsExpression because the type is inferred by almost all database systems based on an expression's final type. In addition, I was thinking at some point specifying expressions from a POCO (similar to how third party package EasyMigrator works), so you could one day pass in an expression tree.
Incorporating expressions at some point in the future would also be a nice way to specify partition formulas. For a long time I didn't see a ton of value in the EasyMigrator approach but with features I want to do in the future like truly idempotent, offline Migrations and ability to test Migrations, having a current representation of what the schema should be described as entities is quite nice.
How confusing do you think it is? I guess the weird part is nobody in the sql world would call it an expression column.
There was a problem hiding this comment.
the weird part is nobody in the sql world would call it an expression column
I think that’s important. I expect one of the goals is to make the library easy to use. If you were a user and were attempting to make a computed / calculated / virtual / generated column, what method name would you look for?
Expression certainly still makes sense for the argument name even if it’s not the name of the method.
I originally suggested AsExpression because the type is inferred by almost all database systems based on an expression's final type
My research indicated that only SQL Server was able to infer the data type; all other databases require an explicit data type. Hence moving the method to an option.
There was a problem hiding this comment.
Ok. I'm out voted 2 to 1. AsComputed
There was a problem hiding this comment.
lol I’m okay with whatever. Just my opinion.
Do you think it should be AsComputed or just Computed? We don’t have AsIdentity which is also a generated column…
There was a problem hiding this comment.
I find Computed may be a better choice especially if we may still need to specify the column type with the "as" methods
There was a problem hiding this comment.
Good point. Computed() reads better.
We could also add an IColumnTypeSyntax.AsComputed<TDbType>(string expression, bool stored = false) that wraps the long-form expression, e.g.
.AsComputed<int>("USER_ID()", true)would be short-form for
.AsInt32().Computed("USER_ID(), true)The relative advantage of the short-form is API discoverability.
ExpressionBuilderWithColumnTypesBase would then be something like:
/// <inheritdoc />
public TNext AsComputed<TDbType>(string expression, bool stored = false)
{
Column.Type = GetDbType<TDbType>();
this.Computed(expression, stored);
return (TNext)(object)this;
}
private DbType GetDbType<TDbType>()
{
var typeCode = Type.GetTypeCode(typeof(TDbType);
switch (typeCode)
{
case TypeCode.String: return DbType.String;
case TypeCode.Int32: return DbType.Int32;
case TypeCode.Int64: return DbType.Int64;
case TypeCode.Decimal: return DbType.Decimal;
// etc
default:
throw new InvalidOperationException("TDbType type code {typeCode} does not map to a FluentMigrator DbType.");
}
}There was a problem hiding this comment.
Thinking about my prototype above a bit more after lunch break - for Decimal and String, this interface is non-ideal since we wouldnt know the length of the string or whether it's an ANSI or UTF string, fixed or variable, or in the case of decimal, the scale and precision.
There was a problem hiding this comment.
Okay, I'll work on the 'long-form' first (when I have time) and then we can always add the short-form as subsequent PR if desired.
|
Hey @Shane32, any news about this pull request ? :) |
|
@PhenX I'll take the blame for the momentum we lost on this PR. October was a busy time for me with the new born and I was probably too fussy with my feedback here. I actually think we're not far from So, relatively good news, @Shane32 changes only had one failing test so far: FluentMigrator.Tests.Unit.Generators.Postgres.PostgresTableTests.CanCreateTableWithMultiColumnPrimaryKeyWithDefaultSchema(STRICT) FluentMigrator.Exceptions.DatabaseOperationNotSupportedException : Virtual computed columns are not supported |
| public string Expression { get; set; } | ||
|
|
||
| /// <inheritdoc /> | ||
| public bool ExpressionStored { get; set; } |
There was a problem hiding this comment.
I don't think it should be true by default
There was a problem hiding this comment.
I designed as follows: Expression is a string containing the computed column SQL expression if it is a computed column; otherwise null. ExpressionStored is always false unless (a) it is a computed column, and (b) the column is persisted/stored (vs a virtual column).
|
@jzabroski I think my proposal to rename the method should be applied before merging though. It will be much more clear imo |
|
Sorry I forgot about this. I can’t remember where we were but I’m glad to pick it up again. |
|
Agree. AsExpression should be renamed to AsComputed |
5b8d3d5 to
1ca1cd6
Compare
| var expression = GeneratorTestHelper.GetAlterColumnExpressionWithComputed(); | ||
|
|
||
| var result = Generator.Generate(expression); | ||
| result.ShouldBe("ALTER TABLE \"public\".\"TestTable1\" ALTER \"TestColumn1\" TYPE ;"); |
There was a problem hiding this comment.
@Shane32 @jzabroski While writing integration tests, I'm telling myself I'm not sure we need to support this, or only the possibility to change the expression of an already computed column. What do you think ?
There was a problem hiding this comment.
I think line 292 is wrong.
- All computed columns in Postgres are STORED.
- You cannot change the type of the computed column.
- You cannot replace a non-computed column with a computed column expression.
We could offer CompatibilityMode.Strict as a way to throw an error if someone tries to do this. If in non-Strict mode, drop the existing column and re-create it.
There was a problem hiding this comment.
Yeah, that's why I asked this, the SQL is clearly wrong, hence @Shane32 's todo
I think we should simply remove theses tests, the SQL error a user might have if he tries that might be explicit enough not for us to throw a custom exception, no ?
There was a problem hiding this comment.
Ideally every Generator follow the same Test naming conventions, so I am not in favor of removing tests. In the past this has caused us to miss regressions when making changes.
| var expression = GeneratorTestHelper.GetAlterColumnExpressionWithComputed(); | ||
|
|
||
| var result = Generator.Generate(expression); | ||
| result.ShouldBe("ALTER TABLE \"public\".\"TestTable1\" ALTER \"TestColumn1\" TYPE ;"); |
| var expression = GeneratorTestHelper.GetAlterColumnExpressionWithComputed(); | ||
|
|
||
| var result = _generator.Generate(expression); | ||
| result.ShouldBe("ALTER TABLE \"public\".\"TestTable1\" ALTER \"TestColumn1\" TYPE ;"); |
|
Anyone know where to find postgres SQL documentation for altering generated columns? This link here only mentions adding generated columns. Or with a sample syntax that we know works, I can update the code to produce expressions of the proper format. |
|
Also, do CI checks automatically run here? Do they test all providers? I'm guessing that locally only SQLite runs by default. I could probably configure MS SQL localdb but I'm not really planning to test any other providers locally. |
I fixed the bug and checked all the similar conditions. Also wrapped all the expressions with parentheses, which is required by SQLite at minimum, and probably other providers too. |
| Create.Table("Products") | ||
| .WithColumn("Id").AsInt32().PrimaryKey().Identity() | ||
| .WithColumn("Price").AsDecimal(10, 2).NotNullable() | ||
| .WithColumn("Quantity").AsInt32().NotNullable() | ||
| .WithColumn("Total").AsDecimal(10, 2).AsExpression("Price * Quantity"); | ||
|
|
||
| Alter.Table("Products") | ||
| .AddColumn("TotalWithTax").AsDecimal(10, 2).AsExpression("Total * 1.1", true); |
There was a problem hiding this comment.
This code will probably need a conditional to only run for providers that support virtual/generated columns. Since it only tests SQLite currently, the test passes. Also, one provider only supports stored/persisted columns, so might need to pass in , true for that provider. Could expand this migration to test altering the column as well, with additional conditionals for providers that support altering virtual/generated columns.
|
@jzabroski Had any time to look at this PR? |
Unfortunately it's broken. I think its because I enabled branch protection. I really dislike how GitHub security works. |
I'm getting my energy back up again. I think this looks good but you have a few outstanding TODOs in your comments from March 10th that need resolving. I will run the pipeline for you now so you can see how the tests perform. |
|
@jzabroski I hope you get better now, what do we do about the last remaining issues ? Do we merge this PR as is and I make a pull request right after with the resolved TODOs ? |
|
These two TODO?
|
|
Sorry, I meant the renaming of "AsExpression" to "Computed", but the todos are of course needed |
|
I prepared the changes here, along with test fixes https://github.com/PhenX/fluentmigrator/tree/computed_columns |
|
Thanks.
|
|
|
I just saw some activity here, so I merged |
|
@jzabroski let's merge this and after I'll make a PR with this what do you think? |
|
I can rebase it manually, and then it will let you 'rebase and merge'. I normally use Squash and Merge myself, so I don't run into this issue. |
d198c9d to
752b1b3
Compare
|
Looking at the merge history, it looks like 'squash and merge' was used in the past, so not sure if it was an intentional change to use 'rebase and merge' now. |
|
@PhenX most likely your branch is now going to have merge conflicts since I rebased this PR... (ugh) it would have been the same with squash and merge though probably ... |
|
@Shane32 no problem :) |
|
@Shane32 Solved via #2131 - I still dont know what went wrong but I think it was a glitch in the GitHub UI. Today, the UI is different, but I am closing this PR. I merged @PhenX #2131 but preserved the commit history so that you get proper credit. I sometimes squash - especially if it helps hide how dumb/rushed I can be :) Sometimes I only have 30 minutes to code on this project and I just commit stuff without running tests, etc. Not how I would code at my day job but somewhat low risk. Most of my deep thought on this project is reviewing PRs and issues vs. getting time to code. Hoping that will change going forward. |
|
Ok no problem. Interesting it didn't look that way on my pc. It said no conflicts... In any case glad it's merged in! |
|
Note: This feature was merged in via: |


Closes #1152
Adds computed column support
Todo:
Notes: