🔨 chore(db): make push_tokens unique constraint device-only

Drop the userId prefix from the push_tokens unique index — one row per
device, reassigned to the new user on switch (upsert by deviceId).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-05-28 12:51:37 +08:00
parent 3675fe05a7
commit addf14c2a6
5 changed files with 10 additions and 16 deletions
+1 -1
View File
@@ -1326,7 +1326,7 @@ table push_tokens {
last_seen_at "timestamp with time zone" [not null, default: `now()`]
indexes {
(user_id, device_id) [name: 'idx_push_tokens_user_device', unique]
device_id [name: 'idx_push_tokens_device', unique]
user_id [name: 'idx_push_tokens_user']
last_seen_at [name: 'idx_push_tokens_last_seen']
}
@@ -39,7 +39,7 @@ ALTER TABLE "push_tokens" DROP CONSTRAINT IF EXISTS "push_tokens_user_id_users_i
ALTER TABLE "push_tokens" ADD CONSTRAINT "push_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "document_shares_document_id_unique" ON "document_shares" USING btree ("document_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "document_shares_user_id_idx" ON "document_shares" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "idx_push_tokens_user_device" ON "push_tokens" USING btree ("user_id","device_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "idx_push_tokens_device" ON "push_tokens" USING btree ("device_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_push_tokens_user" ON "push_tokens" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_push_tokens_last_seen" ON "push_tokens" USING btree ("last_seen_at");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "topics_model_idx" ON "topics" USING btree ("model");--> statement-breakpoint
@@ -6,7 +6,7 @@
},
"dialect": "postgresql",
"enums": {},
"id": "fe7c1962-3f6f-4c00-b2dd-7ce035fecb9f",
"id": "07e041f5-28d5-443a-b5c5-59aa6122ba48",
"policies": {},
"prevId": "e8bb3ce2-fead-4b3d-810e-307c98e891b7",
"roles": {},
@@ -10001,15 +10001,9 @@
}
},
"indexes": {
"idx_push_tokens_user_device": {
"name": "idx_push_tokens_user_device",
"idx_push_tokens_device": {
"name": "idx_push_tokens_device",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "device_id",
"isExpression": false,
@@ -732,7 +732,7 @@
{
"idx": 104,
"version": "7",
"when": 1779942758520,
"when": 1779943832837,
"tag": "0104_topic_usage_push_tokens_tasks_doc_shares",
"breakpoints": true
}
+4 -4
View File
@@ -6,8 +6,8 @@ import { users } from './user';
/**
* Stores Expo push notification tokens registered by mobile clients.
*
* One row per (userId, deviceId) — a single user may have multiple devices
* (e.g. iPhone + Android tablet), each receiving its own notifications.
* One row per device — on user switch the row is reassigned to the new user
* (re-registration upserts by deviceId).
*
* Tokens are validated at registration time but may become invalid over time
* (app uninstall, OS reinstall). Cleanup happens via the Expo receipt cron
@@ -38,8 +38,8 @@ export const pushTokens = pgTable(
lastSeenAt: timestamptz('last_seen_at').defaultNow().notNull(),
},
(table) => [
/** Same user + device = one row; re-registration upserts in place */
uniqueIndex('idx_push_tokens_user_device').on(table.userId, table.deviceId),
/** One row per device; re-registration upserts in place */
uniqueIndex('idx_push_tokens_device').on(table.deviceId),
/** PushChannel.deliver fans out by userId */
index('idx_push_tokens_user').on(table.userId),
/** Future: cleanup long-inactive tokens by lastSeenAt */