Skip to content

fix: isolation level and primary user reservation#288

Open
sattvikc wants to merge 31 commits intomasterfrom
feat/isolation-reservation-table
Open

fix: isolation level and primary user reservation#288
sattvikc wants to merge 31 commits intomasterfrom
feat/isolation-reservation-table

Conversation

@sattvikc
Copy link
Copy Markdown
Contributor

Summary of change

(A few sentences about this PR)

Related issues

  • Link to issue1 here
  • Link to issue1 here

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

  • Changelog has been updated
  • pluginInterfaceSupported.json file has been updated (if needed)
  • Changes to the version if needed
    • In build.gradle
  • Had installed and ran the pre-commit hook
  • If there are new dependencies that have been added in build.gradle, please make sure to add them
    in implementationDependencies.json.
  • Issue this PR against the latest non released version branch.
    • To know which one it is, run find the latest released tag (git tag) in the format vX.Y.Z, and then find the
      latest branch (git branch --all) whose X.Y is greater than the latest released tag.
    • If no such branch exists, then create one from the latest released branch.
  • When adding new recipes, ensure that its performance is being measured in the OneMillionUsersTest

Remaining TODOs for this PR

  • Item1
  • Item2

Comment on lines +3555 to +3559
GeneralQueries.linkAccounts_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId);
AccountInfoQueries.reserveAccountInfoForLinking_Transaction(this, sqlCon, appIdentifier, recipeUserId, primaryUserId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is the idea here that we can just drop the GeneralQueries one sooner or later?

Copy link
Copy Markdown
Contributor Author

@sattvikc sattvikc Dec 31, 2025

Choose a reason for hiding this comment

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

yes

Comment on lines +246 to +249
{
io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock(
sqlCon, tenantConfig.tenantIdentifier.getConnectionUriDomain() + "~" + tenantConfig.tenantIdentifier.getAppId() + "~" + tenantConfig.tenantIdentifier.getTenantId());
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need this? What do we want to achieve with this here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we validate the thirdParty.id cannot have "::"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +72 to +74
+ "account_info_value TEXT NOT NULL,"
+ "third_party_id VARCHAR(28),"
+ "third_party_user_id VARCHAR(256),"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please move these right below/above where we add the ? this will replace.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no longer valid

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());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Couldn't we do this similarly to how the addPrimaryUserAccountInfo_Transaction, using a subquery based on the recipe user id?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is now covered

}
}

public static void checkIfLoginMethodsCanBeLinked_Transaction(Start start, TransactionConnection con, AppIdentifier appIdentifier, Set<String> tenantIds, Set<String> emails,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please see if it makes sense to use a subquery here instead of the collected emails

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

if (thirdParties != null && !thirdParties.isEmpty()) {
List<String> thirdPartyValues = new ArrayList<>();
for (LoginMethod.ThirdParty tp : thirdParties) {
thirdPartyValues.add(tp.id + "::" + tp.userId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should extract the concatenation into a single util function, just to ensure this stays consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +319 to +327
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(",");
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please group this together with adding the params.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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, ?"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We may want to add explicit types when using a parameter in the subselect "result" directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Where is it done?

Comment on lines +721 to +731
// - 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);
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this getting the table name as a parameter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed unused function

}
}

public static CanLinkAccountsResult checkIfLoginMethodsCanBeLinked_Transaction(Start start, TransactionConnection con,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need to do this in a transaction? We are not taking locks here as far as I can see.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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, ?"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Where is it done?


// Pre-check conflicts before attempting the INSERT
try {
String accountInfoType = getRecipeUserTenantsConflictTypeForAddTenant(sqlCon, recipeUserTenantsTable, tenantIdentifier, userId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed


// Pre-check conflicts before attempting the INSERT
try {
String[] conflict = getPrimaryUserTenantsConflictForAddTenant(sqlCon, primaryUserTenantsTable, tenantIdentifier, supertokensUserId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

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"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should discuss this as it is a bit confusing, plus ideally I'd like to avoid dependencies on appIdToUserId

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we can make this go away, lets evaluate this based on the perf test numbers.

Comment on lines +964 to +973
+ " 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 <> ?"
+ " )"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
+ " 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 <> ?"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +1154 to +1158
+ " 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 != ?";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
+ " 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 != ?";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +74 to +75
+ "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),"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
+ "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),"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +1199 to +1206
+ " 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 = ?"
+ " )";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's evaluate if we want to change this to match the do update to old value + app level check approach

Comment on lines +731 to +732
// indexes
// TODO
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

TODO

}
}

public static CanLinkAccountsResult checkIfLoginMethodsCanBeLinked_Transaction(Start start,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is starting its own transaction, not matching naming conventions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed

+ "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")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we need the foreign key to include the tenant_id

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated

}
}

public static boolean addPrimaryUserAccountInfo_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier, String userId) throws
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This method is huge. Could you break into smaller ones?

}
}

public static CanBecomePrimaryResult checkIfLoginMethodCanBecomePrimary(Start start, AppIdentifier appIdentifier, String recipeUserId)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This method is rather long. Could you break it into smaller ones?


}

public static boolean reserveAccountInfoForLinking_Transaction(Start start, Connection sqlCon, AppIdentifier appIdentifier,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this method is rather long. Please break it down.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants