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-547 idParam() registers composite id value with a single placeholder #2242

Merged
merged 4 commits into from
Sep 18, 2024

Conversation

bog-walk
Copy link
Member

Description

Summary of the change:
idParam() now registers each value stored in a composite key value.

Detailed description:

  • Why:

Using idParam() with a Column<EntityID<CompositeID>> only invokes registerArgument() once for the entire composite id value, leading to only 1 placeholder '?' being appended to the statement.

  • What:

QueryParameter.toQueryBuilder() now checks whether the value to parameterize is a CompositeID and, if so, calls registerArgument() for every component value.

Using idParam() with equality or null comparison operators is also now possible.

  • How:

Internal property compositeValue has been added to the QueryParameter class, both as a flag (to differentiate between other QueryParameter) and to store the CompositeID instead of always casting first.


Type of Change

Please mark the relevant options with an "X":

  • Bug fix

Affected databases:

  • All

Checklist

  • Unit tests are in place
  • The build is green (including the Detekt check)

Related Issues

EXPOSED-547

Comment on lines -705 to +716
override fun toQueryBuilder(queryBuilder: QueryBuilder): Unit = queryBuilder { registerArgument(sqlType, value) }
internal val compositeValue: CompositeID? = (value as? EntityID<*>)?.value as? CompositeID

override fun toQueryBuilder(queryBuilder: QueryBuilder) {
queryBuilder {
compositeValue?.let {
it.values.entries.appendTo { (column, value) ->
registerArgument(column.columnType, value)
}
} ?: registerArgument(sqlType, value)
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Alternatively we could add a new class like CompositeQueryParameter that would be very similar but might make some branches cleaner. But I couldn't think of any other applications for it so I went this way to keep the logic together.
I also didn't want to open QueryParameter just for an override.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I understand that, I tried to write something like CompositeQueryParameter also (during review of PR), and it does not look significantly better. I also tried to extend it not from QueryParameter but from Expression, what it also does not make it much better.

Comment on lines -515 to +520
fun <T : String?> Expression<T>.isNullOrEmpty() = if (this is Column<*> && isEntityIdentifier()) {
table.mapIdOperator(::IsNullOp)
} else {
IsNullOp(this)
}.or { this@isNullOrEmpty.charLength() eq 0 }
fun <T : String?> Expression<T>.isNullOrEmpty() = IsNullOp(this).or { this@isNullOrEmpty.charLength() eq 0 }
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure why this logic was added, as the function signature means it's not possible to call isNullOrEmpty() on an IdTable.id, so the first branch will always be false. charLength() also cannot be invoked on IdTable.id.

assertEquals("(${fullIdentity(Towns.zipCode)} = ?) AND (${fullIdentity(Towns.name)} = ?)", whereClause1)
assertEquals(4, query1.single()[Towns.population])

val query2 = Towns.selectAll().where { idParam(townAId, Towns.id).isNotNull() }
Copy link
Collaborator

Choose a reason for hiding this comment

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

To be honest, I don't finally understand what this statement means.

It looks like we want to query all the Towns that have non null id (that can not be null, because it's primary key). But even if we want to get all the entries with non null id (composite id), why do we need actual townAId value.

As I can see, the condition where { idParam(townAId, Towns.id).isNotNull() } generates the following sql SELECT towns.zip_code, towns."name", towns.population FROM towns WHERE (towns.zip_code IS NOT NULL) AND (towns."name" IS NOT NULL)

If I replace it with where { Towns.id.isNotNull() } I get the same SQL.

I understand that it's the test for idParam, but this part may confuse in terms of how idParam is used.

@obabichevjb
Copy link
Collaborator

It's not the problem of this PR, but I miss the reason fot idParam() to exist.. I expect that I just don't know something, that's why this question is here.

At the first moment I had a thought that idParam(value, column) is the way to combine the id value with the particular column, to avoid usage of id from one table with another table. But looking at how it's used in the tests I'm already not sure with it.

For example in EntityTests::testEntityIdParam() there 2 usages of that.

The first one: CreditCards.select(idParam(creditCard.id, CreditCards.id)).count() where the actual id is not used, because generated sql looks like SELECT COUNT(*) FROM creditcards

The second one is more interesting: CreditCards.select(CreditCards.spendingLimit).where { CreditCards.id eq idParam(creditCard.id, CreditCards.id) }, but from my perspective if the creditCard.id does not already contain combination of value and column, wrapping this value with idParam(creditCard.id, CreditCards.id) does not add safety, because we still can take the value from the wrong table, or to specify wrong table in the right side, for example idParam(creditCard.id, CreditCards.id).

So I'd say that the main question is do we really need this idParam? I'm not agains it, just don't see the main use case of that function.

@bog-walk
Copy link
Member Author

Yup, I agree that tests should be reflective of usage so I'll drop this and change the existing one to actually use idParam() without losing it via count.

You're correct in that statements are parameterized by default, so using idParam() alone doesn't make much sense. From what I'm seeing, and given that it was implemented from the very beginning, the most reasonable explanation seems to be allowing the use of an id value in a function/operator that requires an Expression:

tester.selectAll().where { tester.id eq idParam(id1, tester.id) }
// above is equivalent to below
tester.selectAll().where { tester.id eq id1 }

// the example below is a bit trivial, but this won't compile
tester.select(coalesce(tester.id, id1)
// so we'd have to do this
tester.select(coalesce(tester.id, idParam(id1, tester.id))

This is the only example instance that I can think of when I'd ever recommend using it to a user.

Current idParam registers the value from an entity id column with a single placeholder
marker. This fix ensures that a placeholder is registered for every component
value stored by CompositeID and that this argument registeration is overriden
and handled by the mapping operator as needed.
… placeholder

Replace creation of new QueryParameters with direct calls to registerArgument()
… placeholder

- Refactor test usage of idParam()
@bog-walk bog-walk force-pushed the bog-walk/fix-composite-idparam branch from f2c2cff to 2565626 Compare September 18, 2024 17:20
… placeholder

- Fix type mismatch in tests due to alias.
@bog-walk bog-walk merged commit 8f8499c into main Sep 18, 2024
5 checks passed
@bog-walk bog-walk deleted the bog-walk/fix-composite-idparam branch September 18, 2024 19:05
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