Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [9.5.5]

- Fix no-op account info updates

## [9.5.4]

- Upgrade dependencies to fix vulnerabilities in dependencies
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
id 'java-test-fixtures'
}

version = "9.5.4"
version = "9.5.5"

repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,54 @@ public static void updateAccountInfo_Transaction(Start start, Connection sqlCon,
String recipeUserTenantsTable = getConfig(start).getRecipeUserTenantsTable();
String recipeUserAccountInfosTable = getConfig(start).getRecipeUserAccountInfosTable();

// 0. No-op guard: updating to the current value must be idempotent. The write
// choreography below only works for actual changes — QUERY_2_INSERT derives
// replacement recipe_user_tenants rows whose PK includes account_info_value,
// so a same-value update collides with the user's own existing rows and the
// PK error is misreported as a duplicate-email/phone conflict. The user is
// locked, so these reads are race-safe.
//
// We only skip when rows of this account_info_type already exist AND no row in
// EITHER table differs from the new value. recipe_user_tenants is the table
// whose PK actually self-collides, so it is the source of truth here; checking
// all rows (rather than one arbitrary row) means an inconsistent state — e.g.
// a stale value left behind by a partial failure — is repaired by the normal
// write path below instead of being skipped over. The null (removal) path is
// naturally idempotent and needs no guard.
if (accountInfoValue != null) {
String QUERY_0_ANY_ROWS = "SELECT 1 FROM " + recipeUserTenantsTable
+ " WHERE app_id = ? AND recipe_user_id = ? AND account_info_type = ? LIMIT 1";
boolean hasExistingRows = execute(sqlCon, QUERY_0_ANY_ROWS, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, userId);
pst.setString(3, accountInfoType.toString());
}, result -> result.next());

if (hasExistingRows) {
String QUERY_0_ANY_DIFFERS = "SELECT 1 FROM " + recipeUserTenantsTable
+ " WHERE app_id = ? AND recipe_user_id = ? AND account_info_type = ?"
+ " AND account_info_value != ?"
+ " UNION ALL"
+ " SELECT 1 FROM " + recipeUserAccountInfosTable
+ " WHERE app_id = ? AND recipe_user_id = ? AND account_info_type = ?"
+ " AND account_info_value != ?"
+ " LIMIT 1";
boolean anyRowDiffers = execute(sqlCon, QUERY_0_ANY_DIFFERS, pst -> {
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, userId);
pst.setString(3, accountInfoType.toString());
pst.setString(4, accountInfoValue);
pst.setString(5, appIdentifier.getAppId());
pst.setString(6, userId);
pst.setString(7, accountInfoType.toString());
pst.setString(8, accountInfoValue);
}, result -> result.next());
if (!anyRowDiffers) {
return;
}
}
}

// Note: No need to query for primaryUserId - we already have it from LockedUser.
// The lock guarantees the state hasn't changed since lock acquisition.

Expand Down
Loading