Skip to content
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

fix! EXPOSED-458 Stop sending default and null values in insert state… #2295

Merged

Conversation

obabichevjb
Copy link
Collaborator

With this PR insert, upsert and replace SQL commands will not propogate default values that could be created on the database by default.

The main change is in the logic of how arguments of statements are calculated. Instead of taking all the defaults only client defaults are taken now. The method valuesAndDefaults() that was used for creating arguments was deprecated, because it's open and could be used by other people. Also were added some bug fixes for default values.


Type of Change

  • Bug fix

Updates/remove existing public API methods:

  • Is breaking change

Affected databases:

  • All

Related Issues

EXPOSED-458 Stop sending default and null values in insert statements

@@ -109,24 +108,16 @@ class UpsertTests : DatabaseTestsBase() {

withTables(excludeSettings = TestDB.ALL_H2_V1, tester) { testDb ->
val primaryKeyValues = Pair("User A", "Key A")
if (testDb == TestDB.ORACLE) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oracle does not throw an error here, because for the case of autogenerated onUpdate part the key columns are filtered (filtered for Oracle only)

// `amount` must be passed explicitly now due to usage of that column inside the custom onUpdate statement
// There is an option to call `tester.amount.defaultValueFun?.let { it() }!!`,
// it looks ugly but prevents regression on changes in default value
it[amount] = 25
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was one of my biggest concern on upsert statement changes. The main problem is that that column amount is used inside onUpdate section (and here this section is not autogenerated). Before the changes in the PR it was working, because amount with default value was propagated automatically.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a large problem, as long as this change is well and clearly documented.
The amount being intentionally excluded here is only contingent on the fact that it is actually being set in the insert statement. I don't believe that a user will intentionally use insertValue() in the onUpdate arg unless they either explicitly want to reference an insert value that they are manually setting anyway or they are aware that Exposed actually includes the column in the statement and are taking advantage of this.
Either way, choosing to use insertValue() means accepting the syntax contract, which states that the referenced column must be included in the insert clause.
So they may be surprised, but I doubt it would be considered unfair or illogical. And if they still want the original behavior, I believe it would still be achievable by defining:
integer("amount").default(25).clientDefault { 25 }.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I will add a hint about that in the documentation

About the variant integer("amount").default(25).clientDefault { 25 }, I'd prefer to avoid such a difinition of course, and (not sure, but it can be that) clientDefault() may overwrite default() because they modify the same fields of column

@@ -366,7 +366,7 @@ class CreateMissingTablesAndColumnsTests : DatabaseTestsBase() {
actual.forEach { exec(it) }
} else {
SchemaUtils.drop(whiteSpaceTable)
SchemaUtils.create(whiteSpaceTable)
SchemaUtils.create(emptyTable)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the error in the test, but it didn't occured before because database side defaults were not used.

@obabichevjb obabichevjb requested review from bog-walk, joc-a and e5l November 4, 2024 12:11
Comment on lines -370 to +376
val acceptableDefaults = mutableListOf("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()", "NOW()", "CURRENT_TIMESTAMP(6)", "NOW(6)")
return e.toString().trim() in acceptableDefaults && isFractionDateTimeSupported()

// This check is quite optimistic, it will not allow to create a varchar columns with "CURRENT_DATE" default value for example
// Comparing to the previous variant with white list of functions the new variant does not reject valid values,
// it could be checked on the test UpsertTests::testUpsertWithColumnExpressions()
return e.toString().trim() !in notAcceptableDefaults
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I'm understanding correctly:

  • Whenever any invalid column default value is used (for example, CURRENT_DATE with MySQL v5 & no client default), then the behavior is that Exposed just logs an error and defines the column with SQL NULL in the CREATE ddl instead. So tests (like the datetime ones below) used to pass, before all these changes, because the insert statements included the current date. And now, the same ddl with NULL marker will be defined, but the insert statement will not include these columns, so a date would never be inserted unless done manually.

This will be a fairly specific breaking change and I'd recommend mend highlighting it whenever the breaking changes docs is updated.
I would also suggest updating the log message that is output in Column.descriptionDdl() to reflect the new behavior more explicitly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'm looking at the math functions test below.
I see that the new behavior (for MySQL v5) will be that any invalid default values (other than current_date) will be included in the ddl, leading to a syntax exception, instead of logging an error and marking as NULL.

It might be an edge case, given the older version, but is the plan to include any new api that could avoid this?
Something like a clientDefaultExpression(), so that the DDL is not affected, but Exposed could automatically set the specified database function on every insert?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main idea of the change that before that change any value that is not in the list (event if it's valid default value) was recognized as invalid default value, and skipped in DDL.

Actually, I can say I see straightforward solution here. At the current moment we have some features to help user if he tries to put non valid default value. That includes:

  • prevent creating DDL with invalid value
  • convert column into nullable one if there is no client default value
  • sending default value in every insert query

My PR removes the third case.

One option is to try to continue what we did before, and for example add not only conversion to nullable column, but also conversion default column into clientDefaultExpression column, or we can throw errors and ask users not to put invalid values into default columns (kind of removing the magic we do for users now), or probably there are other variants I don't see now.

@joc-a do you remember why we added such a logic? Probably you also have some thoughts what better to do if we loose sending all the defaults in inserts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to clientDefaultExpression() as public API, we had already some discussions on it, but those time it was added more for feature completeness (if we have default, clientDefault, and defaultExpression, why we should not add clientDefaultExpresson), but decided that's probably not the thing that is needed by many people.
But with new reality it could be a good idea to add it. I could add it with refactoring of current 3 fields for default values.

// `amount` must be passed explicitly now due to usage of that column inside the custom onUpdate statement
// There is an option to call `tester.amount.defaultValueFun?.let { it() }!!`,
// it looks ugly but prevents regression on changes in default value
it[amount] = 25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a large problem, as long as this change is well and clearly documented.
The amount being intentionally excluded here is only contingent on the fact that it is actually being set in the insert statement. I don't believe that a user will intentionally use insertValue() in the onUpdate arg unless they either explicitly want to reference an insert value that they are manually setting anyway or they are aware that Exposed actually includes the column in the statement and are taking advantage of this.
Either way, choosing to use insertValue() means accepting the syntax contract, which states that the referenced column must be included in the insert clause.
So they may be surprised, but I doubt it would be considered unfair or illogical. And if they still want the original behavior, I believe it would still be achievable by defining:
integer("amount").default(25).clientDefault { 25 }.

@e5l e5l removed their request for review November 7, 2024 06:48
@obabichevjb obabichevjb requested a review from bog-walk November 8, 2024 12:28
Copy link
Member

@bog-walk bog-walk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just had some questions.

…lumnValuePreferredFromResultSet with deprecation
@obabichevjb obabichevjb force-pushed the obabichev/exposed-458-no-default-values-in-insert-statemnt branch from f3bdac8 to efe92ad Compare November 13, 2024 09:05
@obabichevjb obabichevjb requested a review from bog-walk November 13, 2024 12:35
@obabichevjb obabichevjb merged commit 77548ed into main Nov 20, 2024
5 checks passed
@obabichevjb obabichevjb deleted the obabichev/exposed-458-no-default-values-in-insert-statemnt branch November 29, 2024 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants