Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e5dcad6
fix: change to read committed
sattvikc Nov 27, 2025
0125d22
fix: new tables for reservation
sattvikc Dec 8, 2025
3234b87
fix: signup
sattvikc Dec 9, 2025
ad7d6b2
fix: refactor account info queries
sattvikc Dec 10, 2025
0f48674
fix: refactor account info queries
sattvikc Dec 10, 2025
2930e09
fix: can and create primary
sattvikc Dec 11, 2025
98582a7
fix: link accounts
sattvikc Dec 12, 2025
854a1a3
fix: associate tenant
sattvikc Dec 12, 2025
b6b5b2c
fix: tenant disassociation
sattvikc Dec 18, 2025
27f3ded
fix: delete user and bug fixes
sattvikc Dec 18, 2025
4c211e0
fix: tp fix
sattvikc Dec 18, 2025
768ecb1
fix: fkey
sattvikc Dec 19, 2025
c33b8d5
fix: unlink accounts and bug fixes
sattvikc Dec 19, 2025
df5c584
fix: email change
sattvikc Dec 24, 2025
0d4cd8d
fix: bulk import
sattvikc Dec 26, 2025
e2868b1
fix: compile
sattvikc Dec 26, 2025
ae89e02
fix: index
sattvikc Dec 30, 2025
854414b
fix: using results for can make primary and can link
sattvikc Jan 2, 2026
5e69e02
fix: bulk primary and link accounts
sattvikc Jan 5, 2026
a050582
fix: update plugin version
sattvikc Jan 6, 2026
2f3490f
fix: review comments
sattvikc Jan 7, 2026
cc411fb
fix: refactor create primary
sattvikc Jan 7, 2026
a135aac
fix: account info table
sattvikc Jan 12, 2026
bae0bc6
fix: bulk import impl
sattvikc Jan 14, 2026
77ec671
fix: review comments
sattvikc Jan 19, 2026
5edff93
fix: bulk import error handling
sattvikc Jan 19, 2026
4b2a0c6
fix: cleanup
sattvikc Jan 19, 2026
0675cd7
fix: cleanup
sattvikc Jan 20, 2026
e86ed96
fix: deadlock tests
sattvikc Jan 21, 2026
e9019bc
fix: deadlock tests
sattvikc Jan 21, 2026
21b3043
Merge remote-tracking branch 'origin/9.4' into feat/isolation-reserva…
porcellus Feb 19, 2026
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id 'java-library'
}

version = "9.3.0"
version = "9.4.0"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion pluginInterfaceSupported.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "contains a list of plugin interfaces branch names that this core supports",
"versions": [
"8.3"
"8.4"
]
}
27 changes: 27 additions & 0 deletions src/main/java/io/supertokens/storage/postgresql/LockFailure.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2020, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package io.supertokens.storage.postgresql;

public class LockFailure extends Exception {
public LockFailure() {
super("Failed to acquire advisory lock");
}

public LockFailure(String message) {
super(message);
}
}
484 changes: 315 additions & 169 deletions src/main/java/io/supertokens/storage/postgresql/Start.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,17 @@ public String getBulkImportUsersTable() {
return addSchemaAndPrefixToTableName("bulk_import_users");
}

public String getRecipeUserAccountInfosTable() {
return addSchemaAndPrefixToTableName("recipe_user_account_infos");
}

public String getRecipeUserTenantsTable() {
return addSchemaAndPrefixToTableName("recipe_user_tenants");
}

public String getPrimaryUserTenantsTable() {
return addSchemaAndPrefixToTableName("primary_user_tenants");
}

private String addSchemaAndPrefixToTableName(String tableName) {
return addSchemaToTableName(postgresql_table_names_prefix + tableName);
Expand Down
1,293 changes: 1,293 additions & 0 deletions src/main/java/io/supertokens/storage/postgresql/queries/AccountInfoQueries.java

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,32 @@ public static void createTablesIfNotExists(Start start, Connection con) throws S
update(con, SAMLQueries.getQueryToCreateSAMLClaimsExpiresAtIndex(start), NO_OP_SETTER);
}

if (!doesTableExists(start, con, Config.getConfig(start).getRecipeUserAccountInfosTable())) {
getInstance(start).addState(CREATING_NEW_TABLE, null);
update(con, AccountInfoQueries.getQueryToCreateRecipeUserAccountInfosTable(start), NO_OP_SETTER);

// indexes
// TODO
Comment on lines +731 to +732
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

}

if (!doesTableExists(start, con, Config.getConfig(start).getRecipeUserTenantsTable())) {
getInstance(start).addState(CREATING_NEW_TABLE, null);
update(con, AccountInfoQueries.getQueryToCreateRecipeUserTenantsTable(start), NO_OP_SETTER);

// indexes
update(con, AccountInfoQueries.getQueryToCreateTenantIndexForRecipeUserTenantsTable(start), NO_OP_SETTER);
update(con, AccountInfoQueries.getQueryToCreateRecipeUserIdIndexForRecipeUserTenantsTable(start), NO_OP_SETTER);
update(con, AccountInfoQueries.getQueryToCreateAccountInfoIndexForRecipeUserTenantsTable(start), NO_OP_SETTER);
}

if (!doesTableExists(start, con, Config.getConfig(start).getPrimaryUserTenantsTable())) {
getInstance(start).addState(CREATING_NEW_TABLE, null);
update(con, AccountInfoQueries.getQueryToCreatePrimaryUserTenantsTable(start), NO_OP_SETTER);

// indexes
update(con, AccountInfoQueries.getQueryToCreatePrimaryUserIndexForPrimaryUserTenantsTable(start), NO_OP_SETTER);
}

} catch (Exception e) {
if (e.getMessage().contains("schema") && e.getMessage().contains("does not exist")
&& numberOfRetries < 1) {
Expand Down Expand Up @@ -779,7 +805,10 @@ public static void deleteAllTables(Start start) throws SQLException, StorageQuer
+ getConfig(start).getKeyValueTable() + ","
+ getConfig(start).getAppIdToUserIdTable() + ","
+ getConfig(start).getUserIdMappingTable() + ","
+ getConfig(start).getRecipeUserTenantsTable() + ","
+ getConfig(start).getRecipeUserAccountInfosTable() + ","
+ getConfig(start).getUsersTable() + ","
+ getConfig(start).getPrimaryUserTenantsTable() + ","
+ getConfig(start).getAccessTokenSigningKeysTable() + ","
+ getConfig(start).getTenantFirstFactorsTable() + ","
+ getConfig(start).getTenantRequiredSecondaryFactorsTable() + ","
Expand Down Expand Up @@ -875,8 +904,12 @@ public static KeyValueInfo getKeyValue(Start start, TenantIdentifier tenantIdent
public static KeyValueInfo getKeyValue_Transaction(Start start, Connection con, TenantIdentifier tenantIdentifier,
String key)
throws SQLException, StorageQueryException {

io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock(
con, tenantIdentifier.getAppId() + "~" + tenantIdentifier.getTenantId() + "~" + key);

String QUERY = "SELECT value, created_at_time FROM " + getConfig(start).getKeyValueTable()
+ " WHERE app_id = ? AND tenant_id = ? AND name = ? FOR UPDATE";
+ " WHERE app_id = ? AND tenant_id = ? AND name = ?";

return execute(con, QUERY, pst -> {
pst.setString(1, tenantIdentifier.getAppId());
Expand Down Expand Up @@ -1398,27 +1431,33 @@ public static void linkAccounts_Transaction(Start start, Connection sqlCon, AppI
throws SQLException, StorageQueryException {
{
String QUERY = "UPDATE " + getConfig(start).getUsersTable() +
" SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " +
"user_id = ?";
" SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = (" +
" SELECT primary_user_id FROM " + getConfig(start).getRecipeUserAccountInfosTable() +
" WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" +
") WHERE app_id = ? AND user_id = ?";

update(sqlCon, QUERY, pst -> {
pst.setString(1, primaryUserId);
pst.setString(2, appIdentifier.getAppId());
pst.setString(3, recipeUserId);
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, primaryUserId);
pst.setString(3, appIdentifier.getAppId());
pst.setString(4, recipeUserId);
});
}

updateTimeJoinedForPrimaryUser_Transaction(start, sqlCon, appIdentifier, primaryUserId);

{
String QUERY = "UPDATE " + getConfig(start).getAppIdToUserIdTable() +
" SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = ? WHERE app_id = ? AND " +
"user_id = ?";
" SET is_linked_or_is_a_primary_user = true, primary_or_recipe_user_id = (" +
" SELECT primary_user_id FROM " + getConfig(start).getRecipeUserAccountInfosTable() +
" WHERE app_id = ? AND recipe_user_id = ? LIMIT 1" +
") WHERE app_id = ? AND user_id = ?";

update(sqlCon, QUERY, pst -> {
pst.setString(1, primaryUserId);
pst.setString(2, appIdentifier.getAppId());
pst.setString(3, recipeUserId);
pst.setString(1, appIdentifier.getAppId());
pst.setString(2, primaryUserId);
pst.setString(3, appIdentifier.getAppId());
pst.setString(4, recipeUserId);
});
}
}
Expand Down Expand Up @@ -1515,29 +1554,6 @@ public static void unlinkAccounts_Transaction(Start start, Connection sqlCon, Ap
}
}

public static AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(Start start, Connection sqlCon,
AppIdentifier appIdentifier,
String phoneNumber)
throws SQLException, StorageQueryException {
// we first lock on the table based on phoneNumber and tenant - this will ensure that any other
// query happening related to the account linking on this phone number / tenant will wait for this to finish,
// and vice versa.

PasswordlessQueries.lockPhoneAndTenant_Transaction(start, sqlCon, appIdentifier, phoneNumber);

// now that we have locks on all the relevant tables, we can read from them safely
List<String> userIds = PasswordlessQueries.listUserIdsByPhoneNumber_Transaction(start, sqlCon, appIdentifier,
phoneNumber);

List<AuthRecipeUserInfo> result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier,
userIds);

// this is going to order them based on oldest that joined to newest that joined.
result.sort(Comparator.comparingLong(o -> o.timeJoined));

return result.toArray(new AuthRecipeUserInfo[0]);
}

public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(Start start,
AppIdentifier appIdentifier,
String thirdPartyId,
Expand Down Expand Up @@ -1578,78 +1594,6 @@ public static AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(
return result.toArray(new AuthRecipeUserInfo[0]);
}

public static AuthRecipeUserInfo[] listPrimaryUsersByEmail_Transaction(Start start, Connection sqlCon,
AppIdentifier appIdentifier,
String email)
throws SQLException, StorageQueryException {
// we first lock on the three tables based on email and tenant - this will ensure that any other
// query happening related to the account linking on this email / tenant will wait for this to finish,
// and vice versa.

EmailPasswordQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email);

ThirdPartyQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email);

PasswordlessQueries.lockEmail_Transaction(start, sqlCon, appIdentifier, email);

// now that we have locks on all the relevant tables, we can read from them safely
List<String> userIds = new ArrayList<>();
userIds.addAll(EmailPasswordQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier,
email));

userIds.addAll(PasswordlessQueries.getPrimaryUserIdsUsingEmail_Transaction(start, sqlCon, appIdentifier,
email));

userIds.addAll(ThirdPartyQueries.getPrimaryUserIdUsingEmail_Transaction(start, sqlCon, appIdentifier, email));

String webauthnUserId = WebAuthNQueries.getPrimaryUserIdForAppUsingEmail_Transaction(start, sqlCon,
appIdentifier, email);
if(webauthnUserId != null) {
userIds.add(webauthnUserId);
}

// remove duplicates from userIds
Set<String> userIdsSet = new HashSet<>(userIds);
userIds = new ArrayList<>(userIdsSet);

List<AuthRecipeUserInfo> result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier,
userIds);

// this is going to order them based on oldest that joined to newest that joined.
result.sort(Comparator.comparingLong(o -> o.timeJoined));

return result.toArray(new AuthRecipeUserInfo[0]);
}

public static AuthRecipeUserInfo[] listPrimaryUsersByMultipleEmailsOrPhonesOrThirdParty_Transaction(Start start, Connection sqlCon,
AppIdentifier appIdentifier,
List<String> emails, List<String> phones,
Map<String, String> thirdpartyUserIdToThirdpartyId)
throws SQLException, StorageQueryException {
Set<String> userIds = new HashSet<>();

//collect ids by email
userIds.addAll(EmailPasswordQueries.getPrimaryUserIdsUsingMultipleEmails_Transaction(start, sqlCon, appIdentifier,
emails));
userIds.addAll(PasswordlessQueries.getPrimaryUserIdsUsingMultipleEmails_Transaction(start, sqlCon, appIdentifier,
emails));
userIds.addAll(ThirdPartyQueries.getPrimaryUserIdsUsingMultipleEmails_Transaction(start, sqlCon, appIdentifier, emails));

//collect ids by phone
userIds.addAll(PasswordlessQueries.listUserIdsByMultiplePhoneNumber_Transaction(start, sqlCon, appIdentifier, phones));

//collect ids by thirdparty
userIds.addAll(ThirdPartyQueries.listUserIdsByMultipleThirdPartyInfo_Transaction(start, sqlCon, appIdentifier, thirdpartyUserIdToThirdpartyId));

//collect ids by webauthn
userIds.addAll(WebAuthNQueries.getPrimaryUserIdsUsingEmails_Transaction(start, sqlCon, appIdentifier, emails));

List<AuthRecipeUserInfo> result = getPrimaryUserInfoForUserIds_Transaction(start, sqlCon, appIdentifier,
new ArrayList<>(userIds));

return result.toArray(new AuthRecipeUserInfo[0]);
}

public static AuthRecipeUserInfo[] listPrimaryUsersByEmail(Start start, TenantIdentifier tenantIdentifier,
String email)
throws StorageQueryException, SQLException {
Expand Down Expand Up @@ -1781,17 +1725,6 @@ public static AuthRecipeUserInfo getPrimaryUserInfoForUserId_Transaction(Start s
return result.get(0);
}

public static List<AuthRecipeUserInfo> getPrimaryUserInfosForUserIds_Transaction(Start start, Connection con,
AppIdentifier appIdentifier, List<String> ids)
throws SQLException, StorageQueryException {

List<AuthRecipeUserInfo> result = getPrimaryUserInfoForUserIds_Transaction(start, con, appIdentifier, ids);
if (result.isEmpty()) {
return null;
}
return result;
}

private static List<AuthRecipeUserInfo> getPrimaryUserInfoForUserIds(Start start,
AppIdentifier appIdentifier,
List<String> userIds)
Expand Down Expand Up @@ -1929,7 +1862,6 @@ private static List<AuthRecipeUserInfo> getPrimaryUserInfoForUserIds_Transaction
// for app_id
pst.setString(index, appIdentifier.getAppId());
pst.setString(index+1, appIdentifier.getAppId());
// System.out.println(pst);
}, result -> {
List<AllAuthRecipeUsersResultHolder> parsedResult = new ArrayList<>();
while (result.next()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ public static void overwriteTenantConfig(Start start, TenantConfig tenantConfig)
Connection sqlCon = (Connection) con.getConnection();
{
try {
{
io.supertokens.storage.postgresql.queries.Utils.takeAdvisoryLock(
sqlCon, tenantConfig.tenantIdentifier.getConnectionUriDomain() + "~" + tenantConfig.tenantIdentifier.getAppId() + "~" + tenantConfig.tenantIdentifier.getTenantId());
}
Comment on lines +246 to +249
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.

{
String QUERY = "DELETE FROM " + getConfig(start).getTenantConfigsTable()
+ " WHERE connection_uri_domain = ? AND app_id = ? AND tenant_id = ?;";
Expand Down
Loading