mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Split up last_active_at migration
This commit is contained in:
parent
a809ac16d6
commit
ad802ca279
@ -6,94 +6,3 @@ ALTER TABLE "ProjectUser" ADD COLUMN "lastActiveAt" TIMESTAMP(3);
|
||||
-- Add lastActiveAt and lastActiveAtIpInfo columns to ProjectUserRefreshToken table
|
||||
ALTER TABLE "ProjectUserRefreshToken" ADD COLUMN "lastActiveAt" TIMESTAMP(3);
|
||||
ALTER TABLE "ProjectUserRefreshToken" ADD COLUMN "lastActiveAtIpInfo" JSONB;
|
||||
|
||||
-- SPLIT_STATEMENT_SENTINEL
|
||||
-- SINGLE_STATEMENT_SENTINEL
|
||||
-- CONDITIONALLY_REPEAT_MIGRATION_SENTINEL
|
||||
-- Backfill ProjectUser.lastActiveAt from Events table (using $user-activity events)
|
||||
WITH to_update AS (
|
||||
SELECT pu."tenancyId", pu."projectUserId"
|
||||
FROM "ProjectUser" pu
|
||||
JOIN "Tenancy" t ON t."id" = pu."tenancyId"
|
||||
WHERE pu."lastActiveAt" IS NULL
|
||||
LIMIT 1000
|
||||
),
|
||||
event_activity AS (
|
||||
SELECT
|
||||
t."id" AS "tenancyId",
|
||||
e."data"->>'userId' AS "userId",
|
||||
MAX(e."eventStartedAt") AS "lastActiveAt"
|
||||
FROM "Event" e
|
||||
JOIN "Tenancy" t ON t."projectId" = e."data"->>'projectId'
|
||||
AND t."branchId" = COALESCE(e."data"->>'branchId', 'main')
|
||||
WHERE '$user-activity' = ANY(e."systemEventTypeIds"::text[])
|
||||
AND e."data"->>'userId' IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM to_update tu WHERE tu."tenancyId" = t."id" AND tu."projectUserId"::text = e."data"->>'userId')
|
||||
GROUP BY t."id", e."data"->>'userId'
|
||||
),
|
||||
updated AS (
|
||||
UPDATE "ProjectUser" pu
|
||||
SET "lastActiveAt" = COALESCE(ea."lastActiveAt", pu."createdAt")
|
||||
FROM to_update tu
|
||||
LEFT JOIN event_activity ea ON ea."tenancyId" = tu."tenancyId" AND ea."userId"::uuid = tu."projectUserId"
|
||||
WHERE pu."tenancyId" = tu."tenancyId" AND pu."projectUserId" = tu."projectUserId"
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT COUNT(*) > 0 AS should_repeat_migration FROM updated;
|
||||
|
||||
-- SPLIT_STATEMENT_SENTINEL
|
||||
-- SINGLE_STATEMENT_SENTINEL
|
||||
-- CONDITIONALLY_REPEAT_MIGRATION_SENTINEL
|
||||
-- Backfill ProjectUserRefreshToken.lastActiveAt and lastActiveAtIpInfo from Events table (using $session-activity events)
|
||||
WITH to_update AS (
|
||||
SELECT rt."tenancyId", rt."id"
|
||||
FROM "ProjectUserRefreshToken" rt
|
||||
JOIN "Tenancy" t ON t."id" = rt."tenancyId"
|
||||
WHERE rt."lastActiveAt" IS NULL
|
||||
LIMIT 1000
|
||||
),
|
||||
-- Get the most recent session activity event for each session, along with its IP info
|
||||
event_activity AS (
|
||||
SELECT DISTINCT ON (t."id", e."data"->>'sessionId')
|
||||
t."id" AS "tenancyId",
|
||||
e."data"->>'sessionId' AS "sessionId",
|
||||
e."eventStartedAt" AS "lastActiveAt",
|
||||
CASE
|
||||
WHEN eip."id" IS NOT NULL THEN jsonb_build_object(
|
||||
'ip', eip."ip",
|
||||
'countryCode', eip."countryCode",
|
||||
'regionCode', eip."regionCode",
|
||||
'cityName', eip."cityName",
|
||||
'latitude', eip."latitude",
|
||||
'longitude', eip."longitude",
|
||||
'tzIdentifier', eip."tzIdentifier"
|
||||
)
|
||||
ELSE NULL
|
||||
END AS "ipInfo"
|
||||
FROM "Event" e
|
||||
JOIN "Tenancy" t ON t."projectId" = e."data"->>'projectId'
|
||||
AND t."branchId" = COALESCE(e."data"->>'branchId', 'main')
|
||||
LEFT JOIN "EventIpInfo" eip ON eip."id" = e."endUserIpInfoGuessId"
|
||||
WHERE '$session-activity' = ANY(e."systemEventTypeIds"::text[])
|
||||
AND e."data"->>'sessionId' IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM to_update tu WHERE tu."tenancyId" = t."id" AND tu."id"::text = e."data"->>'sessionId')
|
||||
ORDER BY t."id", e."data"->>'sessionId', e."eventStartedAt" DESC
|
||||
),
|
||||
updated AS (
|
||||
UPDATE "ProjectUserRefreshToken" rt
|
||||
SET "lastActiveAt" = COALESCE(ea."lastActiveAt", rt."createdAt"),
|
||||
"lastActiveAtIpInfo" = ea."ipInfo"
|
||||
FROM to_update tu
|
||||
LEFT JOIN event_activity ea ON ea."tenancyId" = tu."tenancyId" AND ea."sessionId"::uuid = tu."id"
|
||||
WHERE rt."tenancyId" = tu."tenancyId" AND rt."id" = tu."id"
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT COUNT(*) > 0 AS should_repeat_migration FROM updated;
|
||||
|
||||
-- SPLIT_STATEMENT_SENTINEL
|
||||
-- Make columns NOT NULL with default NOW() for new rows
|
||||
ALTER TABLE "ProjectUser" ALTER COLUMN "lastActiveAt" SET NOT NULL;
|
||||
ALTER TABLE "ProjectUser" ALTER COLUMN "lastActiveAt" SET DEFAULT NOW();
|
||||
|
||||
ALTER TABLE "ProjectUserRefreshToken" ALTER COLUMN "lastActiveAt" SET NOT NULL;
|
||||
ALTER TABLE "ProjectUserRefreshToken" ALTER COLUMN "lastActiveAt" SET DEFAULT NOW();
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
-- SINGLE_STATEMENT_SENTINEL
|
||||
-- CONDITIONALLY_REPEAT_MIGRATION_SENTINEL
|
||||
-- Backfill ProjectUser.lastActiveAt from Events table
|
||||
-- Uses correlated subquery with existing index on (projectId, branchId, userId, eventStartedAt)
|
||||
WITH to_update AS (
|
||||
SELECT pu."tenancyId", pu."projectUserId", t."projectId", t."branchId"
|
||||
FROM "ProjectUser" pu
|
||||
JOIN "Tenancy" t ON t."id" = pu."tenancyId"
|
||||
WHERE pu."lastActiveAt" IS NULL
|
||||
LIMIT 10000
|
||||
),
|
||||
updated AS (
|
||||
UPDATE "ProjectUser" pu
|
||||
SET "lastActiveAt" = COALESCE(
|
||||
(
|
||||
SELECT e."eventStartedAt"
|
||||
FROM "Event" e
|
||||
WHERE e."data"->>'projectId' = tu."projectId"
|
||||
AND COALESCE(e."data"->>'branchId', 'main') = tu."branchId"
|
||||
AND e."data"->>'userId' = tu."projectUserId"::text
|
||||
ORDER BY e."eventStartedAt" DESC
|
||||
LIMIT 1
|
||||
),
|
||||
pu."createdAt"
|
||||
)
|
||||
FROM to_update tu
|
||||
WHERE pu."tenancyId" = tu."tenancyId" AND pu."projectUserId" = tu."projectUserId"
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT COUNT(*) > 0 AS should_repeat_migration FROM updated;
|
||||
@ -0,0 +1,12 @@
|
||||
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
|
||||
-- SINGLE_STATEMENT_SENTINEL
|
||||
-- Create temporary index for session backfill that matches our query exactly
|
||||
-- The existing session index has branchId before userId/sessionId, which breaks index usage when we use COALESCE on branchId
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_event_session_backfill_temp
|
||||
ON "Event" (
|
||||
(data->>'projectId'),
|
||||
(COALESCE(data->>'branchId', 'main')),
|
||||
(data->>'userId'),
|
||||
(data->>'sessionId'),
|
||||
"eventStartedAt" DESC
|
||||
);
|
||||
@ -0,0 +1,53 @@
|
||||
-- SINGLE_STATEMENT_SENTINEL
|
||||
-- CONDITIONALLY_REPEAT_MIGRATION_SENTINEL
|
||||
-- Backfill ProjectUserRefreshToken.lastActiveAt and lastActiveAtIpInfo from Events table
|
||||
-- Uses correlated subquery with temporary index on (projectId, COALESCE(branchId, 'main'), userId, sessionId, eventStartedAt DESC)
|
||||
WITH to_update AS (
|
||||
SELECT rt."tenancyId", rt."id", rt."projectUserId", t."projectId", t."branchId"
|
||||
FROM "ProjectUserRefreshToken" rt
|
||||
JOIN "Tenancy" t ON t."id" = rt."tenancyId"
|
||||
WHERE rt."lastActiveAt" IS NULL
|
||||
LIMIT 10000
|
||||
),
|
||||
updated AS (
|
||||
UPDATE "ProjectUserRefreshToken" rt
|
||||
SET "lastActiveAt" = COALESCE(
|
||||
(
|
||||
SELECT e."eventStartedAt"
|
||||
FROM "Event" e
|
||||
WHERE e."data"->>'projectId' = tu."projectId"
|
||||
AND COALESCE(e."data"->>'branchId', 'main') = tu."branchId"
|
||||
AND e."data"->>'userId' = tu."projectUserId"::text
|
||||
AND e."data"->>'sessionId' = tu."id"::text
|
||||
ORDER BY e."eventStartedAt" DESC
|
||||
LIMIT 1
|
||||
),
|
||||
rt."createdAt"
|
||||
),
|
||||
"lastActiveAtIpInfo" = (
|
||||
SELECT CASE
|
||||
WHEN eip."id" IS NOT NULL THEN jsonb_build_object(
|
||||
'ip', eip."ip",
|
||||
'countryCode', eip."countryCode",
|
||||
'regionCode', eip."regionCode",
|
||||
'cityName', eip."cityName",
|
||||
'latitude', eip."latitude",
|
||||
'longitude', eip."longitude",
|
||||
'tzIdentifier', eip."tzIdentifier"
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
FROM "Event" e
|
||||
LEFT JOIN "EventIpInfo" eip ON eip."id" = e."endUserIpInfoGuessId"
|
||||
WHERE e."data"->>'projectId' = tu."projectId"
|
||||
AND COALESCE(e."data"->>'branchId', 'main') = tu."branchId"
|
||||
AND e."data"->>'userId' = tu."projectUserId"::text
|
||||
AND e."data"->>'sessionId' = tu."id"::text
|
||||
ORDER BY e."eventStartedAt" DESC
|
||||
LIMIT 1
|
||||
)
|
||||
FROM to_update tu
|
||||
WHERE rt."tenancyId" = tu."tenancyId" AND rt."id" = tu."id"
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT COUNT(*) > 0 AS should_repeat_migration FROM updated;
|
||||
@ -0,0 +1,4 @@
|
||||
-- RUN_OUTSIDE_TRANSACTION_SENTINEL
|
||||
-- SINGLE_STATEMENT_SENTINEL
|
||||
-- Drop the temporary session backfill index
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_event_session_backfill_temp;
|
||||
@ -0,0 +1,12 @@
|
||||
-- Handle orphaned rows (rows without a matching Tenancy) by setting lastActiveAt to createdAt
|
||||
-- These rows can't be backfilled from Events since we need Tenancy to find the right project/branch
|
||||
UPDATE "ProjectUser" pu
|
||||
SET "lastActiveAt" = pu."createdAt"
|
||||
WHERE pu."lastActiveAt" IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM "Tenancy" t WHERE t."id" = pu."tenancyId");
|
||||
|
||||
UPDATE "ProjectUserRefreshToken" rt
|
||||
SET "lastActiveAt" = rt."createdAt"
|
||||
WHERE rt."lastActiveAt" IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM "Tenancy" t WHERE t."id" = rt."tenancyId");
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
-- Make columns NOT NULL with default NOW() for new rows
|
||||
ALTER TABLE "ProjectUser" ALTER COLUMN "lastActiveAt" SET NOT NULL;
|
||||
ALTER TABLE "ProjectUser" ALTER COLUMN "lastActiveAt" SET DEFAULT NOW();
|
||||
|
||||
ALTER TABLE "ProjectUserRefreshToken" ALTER COLUMN "lastActiveAt" SET NOT NULL;
|
||||
ALTER TABLE "ProjectUserRefreshToken" ALTER COLUMN "lastActiveAt" SET DEFAULT NOW();
|
||||
|
||||
@ -132,6 +132,10 @@ export async function applyMigrations(options: {
|
||||
const isSingleStatement = statement.includes('SINGLE_STATEMENT_SENTINEL');
|
||||
const isConditionallyRepeatMigration = statement.includes('CONDITIONALLY_REPEAT_MIGRATION_SENTINEL');
|
||||
|
||||
if (isConditionallyRepeatMigration && !isSingleStatement) {
|
||||
throw new StackAssertionError("CONDITIONALLY_REPEAT_MIGRATION_SENTINEL requires SINGLE_STATEMENT_SENTINEL", { statement });
|
||||
}
|
||||
|
||||
log(` |> Running statement${isSingleStatement ? "" : "s"}${runOutside ? " outside of transaction" : ""}: ${statement.replace(/(\n|\s)/gm, " ").slice(0, 20)}...`);
|
||||
|
||||
const txOrPrismaClient = runOutside ? options.prismaClient : tx;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user