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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

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 }.

@@ -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.

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?

// `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 }.

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