fix: isolation level and primary user reservation#288
fix: isolation level and primary user reservation#288
Conversation
| GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId); | ||
| AccountInfoQueries.reserveAccountInfoForLinking_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId); |
There was a problem hiding this comment.
Is the idea here that we can just drop the GeneralQueries one sooner or later?
| { | ||
| io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock( | ||
| sqlCon, tenantConfig.tenantIdentifier.getConnectionUriDomain() + "~" + tenantConfig.tenantIdentifier.getAppId() + "~" + tenantConfig.tenantIdentifier.getTenantId()); | ||
| } |
There was a problem hiding this comment.
Do we need this? What do we want to achieve with this here?
There was a problem hiding this comment.
tenant information is updated by deleting all existing information and then re-adding it (across multiple tables). we do not want any parallel createOrUpdate for a given tenant.
| // Insert row for third party id | ||
| AccountInfoQueries.addRecipeUserAccountInfo_Transaction(start, sqlCon, tenantIdentifier, id, | ||
| THIRD_PARTY.toString(), ACCOUNT_INFO_TYPE.THIRD_PARTY, "", "", | ||
| thirdParty.id + "::" + thirdParty.userId); |
There was a problem hiding this comment.
Do we validate the thirdParty.id cannot have "::"?
There was a problem hiding this comment.
why does that even matter? we are never going to try to decode thirdparty id and user id from this value. It may matter when thirdPartyId ended with :: in one case and the user id started with ::. I don't see that as a regular usecase any way. We could use any thing in-between instead of ::, can change if you prefer anything specifically.
| + "account_info_value TEXT NOT NULL," | ||
| + "third_party_id VARCHAR(28)," | ||
| + "third_party_user_id VARCHAR(256)," |
There was a problem hiding this comment.
We could also add a check constraint about "valid combinations" (i.e., tp+email or email or phone), but I don't think we actually need that. Please think it through and document the decision.
| List<Object> parameters = new ArrayList<>(); | ||
|
|
||
| // Add app_id parameter | ||
| parameters.add(appIdentifier.getAppId()); |
There was a problem hiding this comment.
Please move these right below/above where we add the ? this will replace.
| Connection sqlCon = (Connection) con.getConnection(); | ||
|
|
||
| // Build the query dynamically based on which values are not null | ||
| StringBuilder QUERY = new StringBuilder("SELECT primary_user_id, account_info_type FROM " + getConfig(start).getPrimaryUserTenantsTable()); |
There was a problem hiding this comment.
Couldn't we do this similarly to how the addPrimaryUserAccountInfo_Transaction, using a subquery based on the recipe user id?
There was a problem hiding this comment.
this is now covered
| } | ||
| } | ||
|
|
||
| public static void checkIfLoginMethodsCanBeLinked_Transaction(Start start, TransactionConnection con, AppIdentifier appIdentifier, Set<String> tenantIds, Set<String> emails, |
There was a problem hiding this comment.
Please see if it makes sense to use a subquery here instead of the collected emails
| if (thirdParties != null && !thirdParties.isEmpty()) { | ||
| List<String> thirdPartyValues = new ArrayList<>(); | ||
| for (LoginMethod.ThirdParty tp : thirdParties) { | ||
| thirdPartyValues.add(tp.id + "::" + tp.userId); |
There was a problem hiding this comment.
We should extract the concatenation into a single util function, just to ensure this stays consistent.
| StringBuilder QUERY = new StringBuilder("SELECT primary_user_id, account_info_type, account_info_value FROM "); | ||
| QUERY.append(getConfig(start).getPrimaryUserTenantsTable()); | ||
| QUERY.append(" WHERE app_id = ? AND tenant_id IN ("); | ||
| for (int i = 0; i < tenantIdsList.size(); i++) { | ||
| QUERY.append("?"); | ||
| if (i != tenantIdsList.size() - 1) { | ||
| QUERY.append(","); | ||
| } | ||
| } |
There was a problem hiding this comment.
Please group this together with adding the params.
There was a problem hiding this comment.
no longer applicable
| // 1) recipe user's account info -> all tenants of primary user | ||
| String QUERY_1 = "INSERT INTO " + primaryUserTenantsTable | ||
| + " (app_id, tenant_id, account_info_type, account_info_value, primary_user_id)" | ||
| + " SELECT ?, primary_tenants.tenant_id, recipe_ai.account_info_type, recipe_ai.account_info_value, ?" |
There was a problem hiding this comment.
We may want to add explicit types when using a parameter in the subselect "result" directly
| // - Insert row for email (uses third_party_id + third_party_user_id columns) | ||
| recipeUserTenantsBatch.add(pst -> { | ||
| pst.setString(1, tenantIdentifier.getAppId()); | ||
| pst.setString(2, user.userId); | ||
| pst.setString(3, tenantIdentifier.getTenantId()); | ||
| pst.setString(4, THIRD_PARTY.toString()); | ||
| pst.setString(5, ACCOUNT_INFO_TYPE.EMAIL.toString()); | ||
| pst.setString(6, user.thirdpartyId); | ||
| pst.setString(7, user.thirdpartyUserId); | ||
| pst.setString(8, user.email); | ||
| }); |
There was a problem hiding this comment.
Let's document why we need it, as it is not straightforward at first glance.
| "updateAccountInfo_Transaction should only be called with accountInfoType EMAIL or PHONE_NUMBER"); | ||
| } | ||
|
|
||
| private static String[] getPrimaryUserTenantsConflictForAddTenant(Connection sqlCon, String primaryUserTenantsTable, |
There was a problem hiding this comment.
why is this getting the table name as a parameter?
There was a problem hiding this comment.
removed unused function
| } | ||
| } | ||
|
|
||
| public static CanLinkAccountsResult checkIfLoginMethodsCanBeLinked_Transaction(Start start, TransactionConnection con, |
There was a problem hiding this comment.
Why do we need to do this in a transaction? We are not taking locks here as far as I can see.
There was a problem hiding this comment.
Overall, this is a bit chaotic and hard to read/parse. Couldn't we do this using subqueries? Or would that impact performance?
| // 1) recipe user's account info -> all tenants of primary user | ||
| String QUERY_1 = "INSERT INTO " + primaryUserTenantsTable | ||
| + " (app_id, tenant_id, account_info_type, account_info_value, primary_user_id)" | ||
| + " SELECT ?, primary_tenants.tenant_id, recipe_ai.account_info_type, recipe_ai.account_info_value, ?" |
|
|
||
| // Pre-check conflicts before attempting the INSERT | ||
| try { | ||
| String accountInfoType = getRecipeUserTenantsConflictTypeForAddTenant(sqlCon, recipeUserTenantsTable, tenantIdentifier, userId); |
|
|
||
| // Pre-check conflicts before attempting the INSERT | ||
| try { | ||
| String[] conflict = getPrimaryUserTenantsConflictForAddTenant(sqlCon, primaryUserTenantsTable, tenantIdentifier, supertokensUserId); |
| String recipeUserTenantsTable = getConfig(start).getRecipeUserTenantsTable(); | ||
|
|
||
| // 1. Remove account info that is not contributed by any other linked user. | ||
| String QUERY_1 = "DELETE FROM " + primaryUserTenantsTable + " p" |
There was a problem hiding this comment.
We should discuss this as it is a bit confusing, plus ideally I'd like to avoid dependencies on appIdToUserId
There was a problem hiding this comment.
Dependency on appIdToUserId is removed
| + " (app_id, tenant_id, account_info_type, account_info_value, primary_user_id)" | ||
| + " SELECT r.app_id, r.tenant_id, r.account_info_type, r.account_info_value, ?" | ||
| + " FROM " + recipeUserTenantsTable + " r" | ||
| + " INNER JOIN " + recipeUserAccountInfosTable + " ai" |
There was a problem hiding this comment.
I think we can make this go away, lets evaluate this based on the perf test numbers.
| + " SELECT DISTINCT account_info_type, account_info_value" | ||
| + " FROM " + recipeUserTenantsTable | ||
| + " WHERE app_id = ? AND recipe_user_id IN (" | ||
| + " SELECT recipe_user_id" | ||
| + " FROM " + recipeUserAccountInfosTable | ||
| + " WHERE app_id = ? AND primary_user_id IN (" | ||
| + " SELECT primary_user_id FROM " + recipeUserAccountInfosTable | ||
| + " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" | ||
| + " ) AND recipe_user_id <> ?" | ||
| + " )" |
There was a problem hiding this comment.
| + " SELECT DISTINCT account_info_type, account_info_value" | |
| + " FROM " + recipeUserTenantsTable | |
| + " WHERE app_id = ? AND recipe_user_id IN (" | |
| + " SELECT recipe_user_id" | |
| + " FROM " + recipeUserAccountInfosTable | |
| + " WHERE app_id = ? AND primary_user_id IN (" | |
| + " SELECT primary_user_id FROM " + recipeUserAccountInfosTable | |
| + " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" | |
| + " ) AND recipe_user_id <> ?" | |
| + " )" | |
| + " SELECT account_info_type, account_info_value" | |
| + " FROM " + recipeUserAccountInfosTable | |
| + " WHERE app_id = ? AND primary_user_id IN (" | |
| + " SELECT primary_user_id FROM " + recipeUserAccountInfosTable | |
| + " WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" | |
| + " ) AND recipe_user_id <> ?" |
| + " AND account_info_value IN (" | ||
| + " SELECT account_info_value" | ||
| + " FROM " + recipeUserAccountInfosTable | ||
| + " WHERE app_id = ? AND recipe_user_id = ? AND account_info_type = ?" | ||
| + " ) AND account_info_value != ?"; |
There was a problem hiding this comment.
| + " AND account_info_value IN (" | |
| + " SELECT account_info_value" | |
| + " FROM " + recipeUserAccountInfosTable | |
| + " WHERE app_id = ? AND recipe_user_id = ? AND account_info_type = ?" | |
| + " ) AND account_info_value != ?"; | |
| + " AND account_info_value != ?"; |
| + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "account_info_value", "key") | ||
| + " UNIQUE (app_id, recipe_id, recipe_user_id, account_info_type, third_party_id, third_party_user_id, account_info_value)," |
There was a problem hiding this comment.
| + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "account_info_value", "key") | |
| + " UNIQUE (app_id, recipe_id, recipe_user_id, account_info_type, third_party_id, third_party_user_id, account_info_value)," |
| + " AND NOT EXISTS (" | ||
| + " SELECT 1 FROM " + primaryUserTenantsTable + " p" | ||
| + " WHERE p.app_id = r.app_id" | ||
| + " AND p.tenant_id = r.tenant_id" | ||
| + " AND p.account_info_type = r.account_info_type" | ||
| + " AND p.account_info_value = r.account_info_value" | ||
| + " AND p.primary_user_id = ?" | ||
| + " )"; |
There was a problem hiding this comment.
Let's evaluate if we want to change this to match the do update to old value + app level check approach
| // indexes | ||
| // TODO |
|
|
||
| } else if (isForeignKeyConstraintError(serverMessage, config.getUsersTable(), "tenant_id")) { | ||
| throw new TenantOrAppNotFoundException(usersToImport.get(position).tenantIdentifier); | ||
| throw new TenantOrAppNotFoundException(usersToImport.get(position).appIdentifier.getAsPublicTenantIdentifier()); // TODO get proper tenant id |
| } | ||
| } | ||
|
|
||
| public static CanLinkAccountsResult checkIfLoginMethodsCanBeLinked_Transaction(Start start, |
There was a problem hiding this comment.
This is starting its own transaction, not matching naming conventions.
| + "primary_user_id CHAR(36) NOT NULL," | ||
| + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, null, "pkey") | ||
| + " PRIMARY KEY (app_id, tenant_id, account_info_type, account_info_value)," | ||
| + "CONSTRAINT " + Utils.getConstraintName(schema, tableName, "app_id", "fkey") |
There was a problem hiding this comment.
I think we need the foreign key to include the tenant_id
| } | ||
| } | ||
|
|
||
| public static boolean addPrimaryUserAccountInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws |
There was a problem hiding this comment.
This method is huge. Could you break into smaller ones?
| } | ||
| } | ||
|
|
||
| public static CanBecomePrimaryResult checkIfLoginMethodCanBecomePrimary(Start start, AppIdentifier appIdentifier, String recipeUserId) |
There was a problem hiding this comment.
This method is rather long. Could you break it into smaller ones?
|
|
||
| } | ||
|
|
||
| public static boolean reserveAccountInfoForLinking_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, |
There was a problem hiding this comment.
Huge method. Those steps X comments would be excellent points for breaking this into smaller ones.
| } | ||
| } | ||
|
|
||
| public static void updateAccountInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId, ACCOUNT_INFO_TYPE accountInfoType, String accountInfoValue) |
There was a problem hiding this comment.
this method is rather long. Please break it down.
Summary of change
(A few sentences about this PR)
Related issues
Test Plan
(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your
changes work. Bonus points for screenshots and videos!)
Documentation changes
(If relevant, please create a PR in our docs repo, or create a checklist here
highlighting the necessary changes)
Checklist for important updates
pluginInterfaceSupported.jsonfile has been updated (if needed)build.gradlebuild.gradle, please make sure to add themin
implementationDependencies.json.git tag) in the formatvX.Y.Z, and then find thelatest branch (
git branch --all) whoseX.Yis greater than the latest released tag.OneMillionUsersTestRemaining TODOs for this PR