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 handling of numbers in SQLite query arguments #6619

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

grzesiek2010
Copy link
Member

@grzesiek2010 grzesiek2010 commented Feb 20, 2025

Closes #6617

Why is this the best possible solution? Were any other approaches considered?

When filters include numeric values like age=25, JavaRosa represents the number as a Double (25.0). However, SQLite requires arguments to be strings. To ensure correct comparisons in the database, we need to check if the Double value has a .0 fractional part. If it does, we should first convert it to an Int and then to a String, resulting in 25 instead of 25.0. This prevents issues caused by the redundant decimal part during value comparisons.

How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?

We need to test filters with numbers like age=25. The form that is attached to the issue contains such a filter and that was the cause of the crash.

Do we need any specific form for testing your changes? If so, please attach one.

The from that is attached to the issue can be used.

Does this change require updates to documentation? If so, please file an issue here and include the link below.

No.

Before submitting this PR, please make sure you have:

  • added or modified tests for any new or changed behavior
  • run ./gradlew connectedAndroidTest (or ./gradlew testLab) and confirmed all checks still pass
  • added a comment above any new strings describing it for translators
  • added any new strings with date formatting to DateFormatsTest
  • verified that any code or assets from external sources are properly credited in comments and/or in the about file.
  • verified that any new UI elements use theme colors. UI Components Style guidelines

@@ -87,7 +87,13 @@ class LocalEntitiesFilterStrategy(entitiesRepository: EntitiesRepository) :

return if (candidate != null) {
val child = candidate.nodeSide.steps[0].name.name
val value = candidate.evalContextSide(sourceInstance, evaluationContext) as String
var value = candidate.evalContextSide(sourceInstance, evaluationContext)
Copy link
Member

Choose a reason for hiding this comment

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

Where does the type of value come from? Could it also be any other type like maybe a date type or boolean?

Copy link
Member Author

Choose a reason for hiding this comment

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

In the filter it is just a number like age=25 but JR keeps numbers as doubles so it is 25.0. Before converting it to string we need to get rid of that .0 which I described above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Could it also be any other type like maybe

Yes I was thinking about dates as well. I'm going to check that.

Copy link
Member

Choose a reason for hiding this comment

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

Great, thanks. My recollection is that this was addressed for the simple expression case. I would have expected the same tests related to that to catch this. Is there branching between the simple expression and complex expression (with and/or) case? Am I remembering incorrectly about those tests?

Copy link
Member

Choose a reason for hiding this comment

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

My recollection is that this was addressed for the simple expression case. I would have expected the same tests related to that to catch this. Is there branching between the simple expression and complex expression (with and/or) case? Am I remembering incorrectly about those tests?

It's not clear to me what you're referring to here @lognaturel. What's the simple expression case?

Copy link
Member

Choose a reason for hiding this comment

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

I distinctly remember a conversation you and I had about doubles when you were first writing support for expressions without and/or. I thought this class of problems was addressed there but I see that the form crashes in v2024.3.x too so maybe not?

Copy link
Member

Choose a reason for hiding this comment

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

I distinctly remember a conversation you and I had about doubles when you were first writing support for expressions without and/or. I thought this class of problems was addressed there but I see that the form crashes in v2024.3.x too so maybe not?

Yeah, I don't remember considering it at this level sadly!

@grzesiek2010 grzesiek2010 requested a review from seadowg February 20, 2025 20:20
@grzesiek2010 grzesiek2010 marked this pull request as ready for review February 20, 2025 20:20
@grzesiek2010 grzesiek2010 added the high priority Should be looked at before other PRs/issues label Feb 20, 2025
is Query.Eq -> SqlQuery("$column = ?", arrayOf(value))
is Query.NotEq -> SqlQuery("$column != ?", arrayOf(value))
is Query.Eq -> {
if (value.toDoubleOrNull() != null) {
Copy link
Member

Choose a reason for hiding this comment

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

The general approach here is that if we have a "numeric" (a String that looks like an Int or a Double) value for the query, we convert both to double so we end up with essentially Double = Double right?

One thing that feels off to me is that we'll explicitly have the type (an Int or a Double) in LocalEntitiyFilterStrategy and then we're "erasing" it by converting to a String and then reconverting based on a heuristic. Instead could we add DoubleEq and DoubleNotEq types to Query (I guess the existing ones would be StringEq)? Then we'd just lift Int to Double when creating a Query and toSql wouldn't have to make any guesses.

Copy link
Member

Choose a reason for hiding this comment

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

The test case to write to drive out the above I'm realizing is a filter expression like id = '1234'. This implementation would match an id value of '1234.0' (as both get converted to doubles) whereas only the exact string '1234' should match.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@grzesiek2010 grzesiek2010 requested a review from seadowg February 24, 2025 19:30
Copy link
Member

@seadowg seadowg left a comment

Choose a reason for hiding this comment

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

Liking this! There's one point I'd like to discuss, but I think we're ok on (the assumption that everything is a `Double), and I think we should make one test tweak. This is definitely ready to hit QA though so I'll add "needs testing".

body(
select1Dynamic(
"/data/question",
"instance('things')/root/item[age=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 was a little concerned about only having one test here as my instinct was that we need to consider cases for both Double and Int. However, as far as I can see, JavaRosa always treats numbers as Double types for the purposes of evaluation:

  1. IntegerData carries an Int around, but when a path expression is unpacked (using #unpack) for evaluation it gets converted to a Double
  2. XPathNumericLiteral carries a Double around so integer literals never end up as Int

I was aware of 2, but 1 surprised me. Does this blanket statement around not needing to worry about Int seem true @lognaturel? If so, the nice thing is this single test is enough as all "numeric" data will have the same type.

@@ -8,24 +8,24 @@ import org.odk.collect.shared.Query
class SqlQueryTest {
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking that this test should really be more of an "integration" one and check queries against a temp SQLite DB. That's a change we can follow up with.

}

@Test
fun `#query returns matching entities with numeric eq selection arguments`() {
Copy link
Member

Choose a reason for hiding this comment

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

I'd like to split these 2 tests into 4 so we have individual ones for int and decimal property values.

@seadowg
Copy link
Member

seadowg commented Feb 25, 2025

Just as a note for testing: I think looking at filters that use numbers (both int and decimal) is obviously the first thing to check, but I think checking other "unusual" types like dates would be a good idea as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
high priority Should be looked at before other PRs/issues needs testing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Crash during filling filter choice expressions form
3 participants