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

Fixing case sensitive non latin room name filtering #4352

Merged
merged 12 commits into from
Oct 28, 2021

Conversation

ouchadam
Copy link
Contributor

Fixing #3968

Realm does not support non Latin-1 characters in sorting or filtering operations realm/realm-java#1031

In order to workaround this limitation I've added a dedicated normalized room display name field which is a lowercased raw unicode variant of the input string

  • Adds a normalizedDisplayName field + migration to the SessionStore which includes applying a normalized value straight away
  • Makes the SessionStoreMigration and QueryStringValueProcess classes so that we can inject the normalizer into them
  • Introduces a new Case.NORMALIZED in order to reuse the logic in other queries, other places with constraintless freeform text with insensitive search will have the same issue
BEFORE AFTER
Screenshot_20211027_131839 after-casing
BEFORE AFTER
Screenshot_20211027_131822 Screenshot_20211027_132356

…the content based values

- allows for handling those cases separately for normalisation
…iltering rooms by name

- fixes non latin-1 character set room names from being ignored when searching with inexact casing
@github-actions
Copy link

github-actions bot commented Oct 27, 2021

Unit Test Results

  62 files  +2    62 suites  +2   55s ⏱️ +7s
118 tests +1  118 ✔️ +1  0 💤 ±0  0 ±0 
350 runs  +2  350 ✔️ +2  0 💤 ±0  0 ±0 

Results for commit d344be5. ± Comparison against base commit 54daee2.

♻️ This comment has been updated with latest results.

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

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

Thanks for this PR.
I have one concern about the Realm migration class, else this are just small remarks.

}

fun isNormalized() = this is ContentQueryStringValue && case == Case.NORMALIZED
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be declared as an internal extension.

internal fun QueryStringValue.isNormalized() = this is QueryStringValue.ContentQueryStringValue && case == QueryStringValue.Case.NORMALIZED

Copy link
Contributor Author

Choose a reason for hiding this comment

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


enum class Case {
SENSITIVE,
INSENSITIVE
INSENSITIVE,
NORMALIZED
Copy link
Member

Choose a reason for hiding this comment

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

Maybe document here the meaning and the diff of this 3 values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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


internal object RealmSessionStoreMigration : RealmMigration {
internal class RealmSessionStoreMigration @Inject constructor(
Copy link
Member

Choose a reason for hiding this comment

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

We can have problem with Realm if Migration is not an object. Migration class has to be unique. There is maybe another way to do that (as suggested in the log below).
This observed issue in the past was:

Thread: main, Exception: java.lang.IllegalArgumentException: Configurations cannot be different if used to open the same file. The most likely cause is that equals() and hashCode() are not overridden in the migration class: org.matrix.android.sdk.internal.database.RealmSessionStoreMigration

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good to know! also seems like it can be caused by creating the migration class instance multiple times
https://stackoverflow.com/a/36919305

overriding the equals/hashcode seems like the safer approach

Copy link
Contributor Author

Choose a reason for hiding this comment

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

9949779

included a simple equality test as well

* or filtering by case
* See https://github.com/realm/realm-core/issues/777
*/
var normalizedDisplayName: String? = ""
Copy link
Member

Choose a reason for hiding this comment

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

Can this be updated in the set() of displayName above? (but we will need the normalizer so maybe not possible...)
A setter taking the new displayName and the normalizer would avoid mistake in the future maybe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to avoid calling the normalizer directly in the entity, keeping any logic outside~

what about changing the roomDisplayNameResolver to return a data class with both the resolved name and a normalized version?

data class RoomName(val name: String, val normalizedName: String)

val roomName: RoomName = roomDisplayNameResolver.resolve(...)
entity.setDisplayName(roomName)

...

// entity
fun setDisplayName(roomName: RoomName) {
    if (roomName.name != displayName) {
        displayName = roomName.name
        normalizedDisplayName = roomName.normnalizedName
    }
}

Copy link
Contributor Author

@ouchadam ouchadam Oct 27, 2021

Choose a reason for hiding this comment

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

it's close to your suggestion, it allows us to enforce the display name is always updated with a normalized name without having to pass the normalizer around everywhere
611bf29

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I added a comment in the commit, not sure where it will appear here 🤷‍♂️

fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
return when (queryStringValue) {
is QueryStringValue.NoCondition -> {
Timber.v("No condition to process")
Copy link
Member

Choose a reason for hiding this comment

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

This noisy log could probably be removed :)

private fun ContentQueryStringValue.toRealmValue(): String {
return when (case) {
QueryStringValue.Case.NORMALIZED -> normalizer.normalize(string)
QueryStringValue.Case.SENSITIVE, QueryStringValue.Case.INSENSITIVE -> string
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I would prefer to have one condition per line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done 👍 7b35648

QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE
QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE
QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE
QueryStringValue.Case.SENSITIVE, QueryStringValue.Case.NORMALIZED -> Case.SENSITIVE
Copy link
Member

Choose a reason for hiding this comment

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

same remark

query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
query.equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false)
val query = with(queryStringValueProcessor) {
val query = RoomSummaryEntity.where(realm)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: maybe rewrite it to

        val query = with(queryStringValueProcessor) {
            RoomSummaryEntity.where(realm)
                    .process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
                    .let {
                        if (queryParams.displayName.isNormalized()) {
                            it.process(RoomSummaryEntityFields.NORMALIZED_DISPLAY_NAME, queryParams.displayName)
                        } else {
                            it.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
                        }
                    }
                    .process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
                    .process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
                    .equalTo(RoomSummaryEntityFields.IS_HIDDEN_FROM_USER, false)
        }

to avoid this double val query declaration

roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(realm, roomId)
val roomDisplayName = roomDisplayNameResolver.resolve(realm, roomId)
roomSummaryEntity.displayName = roomDisplayName
roomSummaryEntity.normalizedDisplayName = normalizer.normalize(roomDisplayName)
Copy link
Member

Choose a reason for hiding this comment

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

This is where using the setter would help

@@ -191,7 +191,7 @@ class RoomListViewModel @AssistedInject constructor(
}
updatableQuery?.updateQuery {
it.copy(
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.NORMALIZED)
Copy link
Member

Choose a reason for hiding this comment

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

All the changes above in the SDK for this simple change in the app, this is nice!

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

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

Thanks for the update!

@bmarty bmarty merged commit 868548d into develop Oct 28, 2021
@bmarty bmarty deleted the feature/adm/room-filtering branch October 28, 2021 10:27
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