Skip to content

feat: schema rework — reservation tables, LockedUser, read migration, perf#322

Open
porcellus wants to merge 106 commits into
masterfrom
feat/schema-rework
Open

feat: schema rework — reservation tables, LockedUser, read migration, perf#322
porcellus wants to merge 106 commits into
masterfrom
feat/schema-rework

Conversation

@porcellus
Copy link
Copy Markdown
Collaborator

@porcellus porcellus commented May 28, 2026

Implements supertokens/supertokens-plugin-interface#199. See that PR for context.

Supersedes:

Companion PRs: supertokens/supertokens-plugin-interface#199, supertokens/supertokens-core#1277

porcellus and others added 24 commits March 31, 2026 11:34
…Y mode

In LEGACY mode, recipe_user_account_infos is empty, so the subquery
to get primary_user_id returns NULL. Use the primaryUserId parameter
directly instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t 17)

In LEGACY mode, these methods call per-recipe query methods
(EP, PL, TP, WebAuthn) instead of the unified AccountInfoQueries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The legacy query incorrectly referenced thirdparty_user_to_tenant.email
which doesn't exist. Fixed to use thirdparty_users.email with a 3-way
join (thirdparty_users + all_auth_recipe_users + thirdparty_user_to_tenant).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Multiple query paths were unconditionally reading from new tables
(recipe_user_tenants, recipe_user_account_infos, primary_user_tenants)
which are empty in LEGACY migration mode. This caused:
- Infinite loops in signInUpHelper (empty tenant IDs → retry forever)
- WrongCredentialsException on sign-in (users not found by email)
- Missing email conflict checks on email/password/phone updates
- Missing account linking conflict detection (checkIfLoginMethodCanBecomePrimary, checkIfLoginMethodsCanBeLinked)
- Missing tenant association conflict detection (addTenantIdToPrimaryUser)
- Missing bulk import conflict detection (reservePrimaryUserAccountInfos)

Key changes:
- getTenantIdsForUserIds: read from all_auth_recipe_users in LEGACY mode
- getPrimaryUserInfoForUserIds_Transaction: JOIN on old table for tenant IDs
- listPrimaryUsersByThirdPartyInfo: use thirdparty_users table in LEGACY mode
- updateUsersEmail_Transaction (EP/TP/PL/WebAuthn): add legacy email conflict check
- checkIfLoginMethodCanBecomePrimary: full legacy implementation using old tables
- checkIfLoginMethodsCanBeLinked: full legacy implementation with tenant overlap checks
- makePrimaryUser_Transaction: legacy conflict detection and already-primary check
- linkAccounts_Transaction: legacy conflict detection with resolved primary user ID
- addTenantIdToPrimaryUser_Transaction: legacy email/phone/TP conflict check
- reservePrimaryUserAccountInfos_Transaction: legacy conflict detection for bulk import
- deleteDevicesByPhoneNumber/Email_Transaction: use old table for tenant subquery
- ConnectionPool: add socketTimeout=60 for JDBC connections
- New helper methods: listPrimaryUsersByEmail_legacy_forApp, listPrimaryUsersByPhoneNumber_legacy_forApp, listPrimaryUserIdsByThirdPartyInfo_legacy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation

In LEGACY mode (without reservation tables), detect duplicate account
infos within the same batch of PrimaryUsers before checking against
existing users in old tables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation

Adds MigrationBackfillQueries with per-user backfill logic for all recipe
types (EP, PL, TP, WebAuthn). Implements MigrationBackfillStorage in
Start.java. Includes 10 unit tests and 5 integration tests covering
multi-recipe, multi-tenant, linked accounts, batch sizing, and idempotency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set devConfig.yaml migration_mode to MIGRATED (was commented out,
  defaulting to LEGACY) so race condition tests work correctly
- Add setMigrationModeForTesting() to PostgreSQLConfig for test overrides
- Fix BackfillIntegrationTest: use DUAL_WRITE_READ_OLD mode before user
  creation so all_auth_recipe_users gets populated (backfill JOINs on it)
- Fix backfillCronSkipsInLegacyMode: explicitly set LEGACY mode

REVIEW-005

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- DualWriteConsistencyTest: replace invalid cross-table comparison
  (linked-user rows legitimately diverge between old/new tables) with
  independent checks:
    * checkOldTableConsistency — linking flags, primary_or_recipe_user_id
    * checkReservationConsistency — invariants I1–I6
  3/3 tests passing (was 0/3).
- New: BackfillConcurrencyTest, DeleteUserRaceTest, LegacyModeRaceTest,
  MigrationModeTest covering mode-specific race paths.
- Race tests & RaceTestUtils updated for new invariants and helpers.
…fallback

Implement recipe_user_tenants INSERT/DELETE for EmailPassword, ThirdParty,
and Passwordless in MIGRATED mode. Add COALESCE fallback in
linkAccounts_Transaction for deleted primary user reservation rows.
Document migration_mode config option.
Set DUAL_WRITE_READ_OLD mode in BackfillTest, fix CHAR(36) trim comparison,
increase DeadlockTest executor timeout to 180s, add migration_mode to
devConfig ignore list in PostgresSQLConfigTest.
…ization

Without third_party_id in the WHERE clause, the planner cannot use the
full idx_recipe_user_tenants_account_info index (app_id, tenant_id,
account_info_type, third_party_id, account_info_value) as a point lookup.
It falls back to scanning on (app_id, tenant_id, account_info_type) and
filtering the rest — measured at 31ms vs 0.065ms with the fix (477x).

For email and phone rows, third_party_id is always empty string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…fault

WebAuthNQueries.getUsersInfoUsingIdList was wrapping a pure read in
start.startTransaction, while its EmailPassword/ThirdParty/Passwordless
siblings borrowed a pooled connection directly. That wrapper fired on every
/users/by-accountinfo request via the 4-way fan-out in
GeneralQueries.getPrimaryUserInfoForUserIds_new, producing per-request SET
and SHOW TRANSACTION ISOLATION LEVEL round-trips visible in pg_stats.

Replace the wrapper with a try-with-resources pooled connection, matching
the sibling implementations. Also add HikariCP connectionInitSql to
establish READ COMMITTED as the session default once per physical
connection, preparing for a follow-up that can let startTransactionHelper
skip the isolation dance when the requested level equals the current one.
The composite index idx_recipe_user_tenants_account_info had
third_party_id between account_info_type and account_info_value. For
tparty rows the column is always empty (''), and for email/phone rows
of non-thirdparty recipes it is also empty — the only rows with a
non-empty value are the shadow email rows of thirdparty users. With the
column in the index, getPrimaryUserIdByThirdPartyInfo could only match
the 3-column prefix and had to scan+filter the rest, running ~10x
slower than the equivalent email lookup.

Drop the column from the index so all three account-info lookups
(email, phone, tparty) hit a 4-column point lookup; add the matching
third_party_id='' predicate to the tparty query for symmetry with the
email/phone variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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