-
Notifications
You must be signed in to change notification settings - Fork 133
[WellSQL Migration] Migrate WCRevenueStatsModel
to Room
#14680
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
base: trunk
Are you sure you want to change the base?
Conversation
FYI: This is done in preparation to the subsequent WellSQL to Room table migration.
FYI: This change includes but is not limited to the: - Entity Introduction - DAO Introduction (With Test) - WCAndroidDatabase Migration - Code Adjustments (Store) - Test Adjustments & Fixes (Store)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR migrates the WCRevenueStatsModel
from WellSQL to Room database, part of the broader initiative to modernize the app's data persistence layer.
Key changes include:
- Converting the WellSQL model to a Room entity with primary key constraints
- Implementing a new Room DAO for revenue stats operations
- Updating the store to use the new DAO instead of SQL utilities
- Converting synchronous database operations to suspend functions
Reviewed Changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
WCRevenueStatsModel.kt |
Converted from WellSQL table to Room entity with composite primary key |
RevenueStatsDao.kt |
New Room DAO implementing database operations for revenue stats |
WCAndroidDatabase.kt |
Added revenue stats entity and DAO to Room database configuration |
WCStatsStore.kt |
Updated to use Room DAO instead of WellSQL utilities, made functions suspend |
WellSqlConfig.kt |
Added migration to drop the old WellSQL table |
WCStatsSqlUtils.kt |
Removed obsolete WellSQL utility functions |
Test files | Updated test constructors and mocks to match new data class structure |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
@Entity( | ||
tableName = "RevenueStatsEntity", | ||
primaryKeys = ["localSiteId", "rangeId"], | ||
) | ||
data class WCRevenueStatsModel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The composite primary key using rangeId
could be problematic since rangeId
can be empty string according to the data class. Empty strings in primary keys may cause issues with uniqueness constraints and queries. Consider using a different approach, such as making rangeId
nullable or using a single auto-generated primary key.
@Entity( | |
tableName = "RevenueStatsEntity", | |
primaryKeys = ["localSiteId", "rangeId"], | |
) | |
data class WCRevenueStatsModel( | |
import androidx.room.PrimaryKey | |
import androidx.room.Index | |
@Entity( | |
tableName = "RevenueStatsEntity", | |
indices = [Index(value = ["localSiteId", "rangeId"], unique = true)] | |
) | |
data class WCRevenueStatsModel( | |
@PrimaryKey(autoGenerate = true) val id: Long = 0, |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, you are right but:
- During my testing I couldn't find a way to get
rangeId
come as empty string from the REST API. - Initially, I indeed tried the
localSiteId
+interval
+startDate
+endDate
combination as primary key. But, then decided in favor of using thelocalSiteId
+rangeId
instead. This is because when testing with the former 4 keyed primary key I was getting 8/9 rows and thus couldn't replicate what I was seeing with the previous WellSQL implementation, see below:
BEFORE WCRevenueStatsModel

AFTER RevenueStatsEntity

Now notice that when using the 4 keyed primary key, the 1st and 5th (HOURS
) rows completely match, no matter if that's for a non-custom or custom date and thus data is lost, depending which you selected last. Which, to be honest is not a big deal, but nevertheless I wanted to preserve as much parity between the 2 solutions. 💭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other option would be to stick to artificial primary key (as in WCRevenueStatsModel
), but I think rangeId
should be... id
- a unique value (in context of a site).
I think your choice is correct. But you might want also check this PR (if you haven't yet) that introduces the rangeId
and a check if it's empty or not: https://github.com/wordpress-mobile/WordPress-FluxC-Android/pull/2785/files#diff-3142dcb57a32d3e922e5e96b706565af858c76f424d01ecc16a41af257c37649R135-R153
During my testing I couldn't find a way to get
rangeId
come as empty string from the REST API.
I guess that the only use case for this is when a 3rd party plugin modifies this contract. It's though to assess the risk, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking a look at that @wzieba ! 🙇
The other option would be to stick to artificial primary key (as in WCRevenueStatsModel), but I think rangeId should be... id - a unique value (in context of a site).
I too think rangeId
should be a unique id value. 🤷
I think your choice is correct. But you might want also check this PR (if you haven't yet) that introduces the rangeId and a check if it's empty or not: https://github.com/wordpress-mobile/WordPress-FluxC-Android/pull/2785/files#diff-3142dcb57a32d3e922e5e96b706565af858c76f424d01ecc16a41af257c37649R135-R153
I think I did, but I don't remember anymore... 🫣 Will check again and come back to you on that! 💯
I guess that the only use case for this is when a 3rd party plugin modifies this contract. It's though to assess the risk, though.
True, good thinking, this is not the first time we see that! 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👋 @wzieba !
I think I did, but I don't remember anymore... 🫣 Will check again and come back to you on that! 💯
So, I did investigate that today and came to the conclusion that:
- This FluxC#2785 PR was the one introducing this
rangeId
. But, I don't see any evidence as to why it could be empty. 🤷 - And specific FluxC commit introduces this
rangeId != 0
check. Again, I don't see any evidence as to why thisrangeId
can be0
orempty
. 🤷 - I then went and check all the places this gets used and figured out a way to test it. I found that indeed this gets to be empty when it is used in a widget. ✅ (or wearable, not tested)
- However, instead of reverting to using an artificial primary key, and make this all that bit more complicated, unnecessarily if you ask me, I chose to keep the migration as is and introduce a bit of client functionality to populate this
rangeId
with a non-empty value, following the sameCustom
pattern, using either theWidget
orWear
prefix, followed by aStart
+End
date, the result: 5c457ba

Let me know what you think. 🙏
FYI: Let me also ping a product engineer here as an extra pair of eyes on this specific change. Cc @malinajirka and thanks for helping us out here. 🙇
PS: Btw, this rangeId
solution, even without this change here was working as expected without any crashes, because this revenue widget is quite simple, only targeting today (HOURS
, without a means to change that (or I don't know how). 🤔

@Query( | ||
""" | ||
SELECT * FROM RevenueStatsEntity | ||
WHERE localSiteId = :siteId | ||
AND interval = :granularity | ||
AND startDate = :startDate | ||
AND endDate = :endDate | ||
""" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query uses interval = :granularity
but the parameter is of type StatsGranularity
enum while the database column interval
is a String. Room may not handle this enum-to-string conversion automatically, potentially causing query failures.
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I was thinking of adding a @TypeConverter
for this but wasn't too sure if that's too necessary in this case, @wzieba wdty? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a small @TypeConvert
could be a good decision to not leave this conversion vulnerable to changes of Kotlin or Room (e.g. upper/lower case etc.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the reply @wzieba , convinced, will go ahead and add that small @TypeConvert
. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
libs/fluxc-plugin/src/main/kotlin/org/wordpress/android/fluxc/store/WCStatsStore.kt
Show resolved
Hide resolved
Generated by 🚫 Danger |
📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #14680 +/- ##
============================================
+ Coverage 38.44% 38.45% +0.01%
- Complexity 9886 9891 +5
============================================
Files 2103 2104 +1
Lines 117051 117026 -25
Branches 15651 15649 -2
============================================
+ Hits 44996 45007 +11
+ Misses 67882 67848 -34
+ Partials 4173 4171 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Entity( | ||
tableName = "RevenueStatsEntity", | ||
primaryKeys = ["localSiteId", "rangeId"], | ||
) | ||
data class WCRevenueStatsModel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other option would be to stick to artificial primary key (as in WCRevenueStatsModel
), but I think rangeId
should be... id
- a unique value (in context of a site).
I think your choice is correct. But you might want also check this PR (if you haven't yet) that introduces the rangeId
and a check if it's empty or not: https://github.com/wordpress-mobile/WordPress-FluxC-Android/pull/2785/files#diff-3142dcb57a32d3e922e5e96b706565af858c76f424d01ecc16a41af257c37649R135-R153
During my testing I couldn't find a way to get
rangeId
come as empty string from the REST API.
I guess that the only use case for this is when a 3rd party plugin modifies this contract. It's though to assess the risk, though.
@Query( | ||
""" | ||
SELECT * FROM RevenueStatsEntity | ||
WHERE localSiteId = :siteId | ||
AND interval = :granularity | ||
AND startDate = :startDate | ||
AND endDate = :endDate | ||
""" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a small @TypeConvert
could be a good decision to not leave this conversion vulnerable to changes of Kotlin or Room (e.g. upper/lower case etc.).
FetchRevenueStatsResponsePayload( | ||
it, | ||
StatsGranularity.DAYS, | ||
WCRevenueStatsModel(LocalId(1), "", "", "", "", "", "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I've noticed WCRevenueStatsModel(LocalId(1), "", "", "", "", "", "")
is repeated in this PR. WDYT about creating a factory, test-only extension method? I'm thinking about
fun WCRevenueStatsModel.Companion.fake() = WCRevenueStatsModel(LocalId(1), "", "", "", "", "", "")
and then
WCRevenueStatsModel(LocalId(1), "", "", "", "", "", "") | |
WCRevenueStatsModel.fake() |
Or you can even make it more customisable like revenue
method from RevenueStatsDaoTest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion @wzieba , will do, I also like how you've named it fake()
! 💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…to migrate/wc-revenue-stats-model-to-room
It seems that 'revenueRangeId' it totally app driven, and not REST API driven, and as such, can be completely manipulated, effectively becoming a non-empty value at all times. With this change, we are making sure that when 'revenueRangeId' comes from places like 'Widget' or 'Wear', we are adding the necessary missing information to uniquely identify this revenue range id. We are using this '<Medium><StartDate><EndDate>' pattern. FYI: This extra 'Medium' data was added to easily identify the medium. With or without 'Medium' this would have worked fine anyway. PS: Where needed, 'Medium' is either 'Widget' or 'Wear'. ------------------------------------------------------------------------ PR Comment: https://github.com/woocommerce/woocommerce-android/pull/ 14680#discussion_r2398949681
PR Comment: https://github.com/woocommerce/woocommerce-android/pull/ 14680#discussion_r2398949700
PR Comment: https://github.com/woocommerce/woocommerce-android/pull/ 14680#discussion_r2407770941
…to migrate/wc-revenue-stats-model-to-room
Closes: AINFRA-128
Description
This PR migrates
WCRevenueStatsModel
from WellSQL to Room database.Testing information
My store
+Performance
My store
tab and verify that thePerformance
section is being correctly populated with the expectedRevenue
number.Performance
section, click on theCalendar
icon and selectToday
. Verify that the section's title changes to something likeToday Oct 2, 2025
and that theRevenue
number is updated accordingly.This Week
,This Month
andThis Year
and verify everything is working as expected.App Inspection
, check theRevenueStatsEntity
table, and notice 4 rows being populated, each for every non-custom selection of yours (so far):Today2025-10-022025-10-02
(rangeId)Week to Date2025-09-282025-10-04
(rangeId)Month to Date2025-10-012025-10-31
(rangeId)Year to Date2025-01-012025-12-31
(rangeId)Custom
and edit the section's title picking a specific "single" day likeOct 2 - Oct 2
, clickSAVE
. Verify that the section's title changes to something likeCustom Oct 2, 2025 - Oct 2, 2025
and that theRevenue
number is updated accordingly.Oct 1 - Oct 2
, clickSAVE
.Verify that the section's title changes to something like
Custom Oct 1, 2025 - Oct 2, 2025
and verify theRevenue
number is updated accordingly.Custom2025-10-022025-10-02
(rangeId)Custom2025-10-012025-10-02
(rangeId)Custom2025-09-212025-09-27
(rangeId)Custom2025-09-012025-09-30
(rangeId)Custom2025-01-012025-10-02
(rangeId)FYI: I've also compared the before (
WCRevenueStatsModel
) and after (RevenueStatsEntity
) migration data on those tables after making the exact same date selections and everything LGTM, seeImages/gif
below:Images/gif
BEFORE
WCRevenueStatsModel
AFTER
RevenueStatsEntity
RELEASE-NOTES.txt
if necessary. Use the "[Internal]" label for non-user-facing changes.