Improve multiGet, multiSet and multiMerge SQL operations#732
Improve multiGet, multiSet and multiMerge SQL operations#732fabioh8010 wants to merge 9 commits intoExpensify:mainfrom
Conversation
JKobrynski
left a comment
There was a problem hiding this comment.
I agree with Vicky's comments, other than that LGTM!
|
@tgolen Regarding your comment:
Here's what Claude says: I also can confirm that this approach doesn't work without JSON.stringify – we always need to stringify our JSONS before passing to SQLite. SQLite docs explain about
|
|
Ah, cool. I just misunderstood what |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9f31c1520
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Query to fully replace the nested objects of the DB value. | ||
| const replaceQuery = `UPDATE keyvaluepairs | ||
| SET valueJSON = JSON_REPLACE(valueJSON, ?, JSON(?)) | ||
| SET valueJSON = JSON_REPLACE(valueJSON, ?, ?) |
There was a problem hiding this comment.
Preserve JSON types in JSON_REPLACE argument
In the multiMerge replace-null path, JSON_REPLACE(valueJSON, ?, ?) now binds the replacement as a plain SQL value, and SQLite treats that third argument as text unless it comes from JSON(...). When replaceNullPatches contains non-string values (for example null, numbers, or objects), they are persisted as JSON strings (e.g., "null" or "{...}") instead of real JSON values, which corrupts the stored shape and can break subsequent reads/merges for those keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The patches are being stringified with JSON.stringify() as same as other data that is inserted into SQL
/**
* Transforms the replace null patches into SQL queries to be passed to JSON_REPLACE.
*/
function generateJSONReplaceSQLQueries(key: string, patches: FastMergeReplaceNullPatch[]): string[][] {
const queries = patches.map(([pathArray, value]) => {
const jsonPath = `$.${pathArray.join('.')}`;
return [jsonPath, JSON.stringify(value), key];
});
return queries;
}
There was a problem hiding this comment.
But anyway I'm going to double-check
There was a problem hiding this comment.
The review is correct – for JSON_REPLACE operation SQLite handles data differently:
If the value of a path/value pair is an SQLite TEXT value, then it is normally inserted as a quoted JSON string, even if the string looks like valid JSON. However, if the value is the result of another json function (such as json() or json_array() or json_object()) or if it is the result of the -> operator, then it is interpreted as JSON and is inserted as JSON retaining all of its substructure. Values that are the result of the ->> operator are always interpreted as TEXT and are inserted as a JSON string even if they look like valid JSON.
So I reverted this change in particular and added a comment. 04ac56d
I tested myself and JSON() wrapper is needed there.
Details
Slack proposal: https://expensify.slack.com/archives/C05LX9D6E07/p1770627746422309
This PR implements Solution 2 of the above proposal - Remove redundant
json()wrappersRelated Issues
Expensify/App#80243
Automated Tests
N/A
Manual Tests
Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Screen.Recording.2026-02-24.at.08.46.56-compressed.mov
Android: mWeb Chrome
N/A
iOS: Native
Screen.Recording.2026-02-24.at.08.51.28-compressed.mov
iOS: mWeb Safari
N/A
MacOS: Chrome / Safari
N/A
MacOS: Desktop
N/A