From 01993889b17cb22721ffbef0e85d7d658b293fda Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Sun, 28 Jun 2026 14:53:44 -0700 Subject: [PATCH] fix(db): preserve legacy digest subscriptions --- ...3_normalize_digest_subscription_logins.sql | 26 ++++++++++++++ src/db/repositories.ts | 7 +++- test/unit/product-usage.test.ts | 35 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 migrations/0083_normalize_digest_subscription_logins.sql diff --git a/migrations/0083_normalize_digest_subscription_logins.sql b/migrations/0083_normalize_digest_subscription_logins.sql new file mode 100644 index 0000000000..6600b7345a --- /dev/null +++ b/migrations/0083_normalize_digest_subscription_logins.sql @@ -0,0 +1,26 @@ +CREATE TEMP TABLE digest_subscriptions_canonical AS +SELECT id, lower(login) AS login, lower(email) AS email, status, source, created_at, updated_at +FROM ( + SELECT + id, + login, + email, + status, + source, + created_at, + updated_at, + row_number() OVER ( + PARTITION BY lower(login), lower(email) + ORDER BY updated_at DESC, created_at DESC, id DESC + ) AS rn + FROM digest_subscriptions +) +WHERE rn = 1; + +DELETE FROM digest_subscriptions; + +INSERT INTO digest_subscriptions (id, login, email, status, source, created_at, updated_at) +SELECT id, login, email, status, source, created_at, updated_at +FROM digest_subscriptions_canonical; + +DROP TABLE digest_subscriptions_canonical; diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 5b791437c1..0c7a3889fc 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -1361,7 +1361,12 @@ export async function upsertDigestSubscription( export async function listDigestSubscriptionsForLogin(env: Env, login: string): Promise { const db = getDb(env.DB); - const rows = await db.select().from(digestSubscriptions).where(eq(digestSubscriptions.login, login.toLowerCase())).orderBy(desc(digestSubscriptions.updatedAt)).limit(20); + const rows = await db + .select() + .from(digestSubscriptions) + .where(sql`lower(${digestSubscriptions.login}) = ${login.toLowerCase()}`) + .orderBy(desc(digestSubscriptions.updatedAt)) + .limit(20); return rows.map(toDigestSubscriptionRecord); } diff --git a/test/unit/product-usage.test.ts b/test/unit/product-usage.test.ts index bcdb470a31..a9ef67646f 100644 --- a/test/unit/product-usage.test.ts +++ b/test/unit/product-usage.test.ts @@ -1,3 +1,5 @@ +import { readFileSync, readdirSync } from "node:fs"; +import { DatabaseSync } from "node:sqlite"; import { describe, expect, it } from "vitest"; import { getContributorScoringProfile, @@ -343,6 +345,39 @@ describe("product usage events", () => { ]); }); + it("normalizes legacy digest subscription rows during migration", () => { + const db = new DatabaseSync(":memory:"); + for (const migrationFile of readdirSync("migrations") + .filter((file) => file.endsWith(".sql") && file < "0083_") + .sort()) { + db.exec(readFileSync(`migrations/${migrationFile}`, "utf8")); + } + db.exec(` + INSERT INTO digest_subscriptions (id, login, email, status, source, created_at, updated_at) + VALUES + ('legacy-active', 'OktoFeesh1', 'Digest@Example.com', 'active', 'app', '2026-05-29T00:00:00.000Z', '2026-05-29T00:00:00.000Z'), + ('legacy-paused', 'oktofeesh1', 'digest@example.com', 'paused', 'app', '2026-05-30T00:00:00.000Z', '2026-05-30T00:00:00.000Z') + `); + + db.exec(readFileSync("migrations/0083_normalize_digest_subscription_logins.sql", "utf8")); + + expect(db.prepare("SELECT login, email, status FROM digest_subscriptions").all()).toEqual([ + { login: "oktofeesh1", email: "digest@example.com", status: "paused" }, + ]); + }); + + it("keeps legacy mixed-case digest subscriptions visible during lookup", async () => { + const env = createTestEnv(); + await env.DB.prepare( + `INSERT INTO digest_subscriptions (id, login, email, status, source, created_at, updated_at) + VALUES ('legacy-digest', 'OktoFeesh1', 'legacy@example.com', 'active', 'app', '2026-05-30T00:00:00.000Z', '2026-05-30T00:00:00.000Z')`, + ).run(); + + await expect(listDigestSubscriptionsForLogin(env, "oktofeesh1")).resolves.toEqual([ + expect.objectContaining({ login: "OktoFeesh1", email: "legacy@example.com", status: "active" }), + ]); + }); + it("summarizes recent events without counting stale records", async () => { const env = createTestEnv({ PRODUCT_USAGE_HASH_SALT: "fixed-test-salt" }); await recordProductUsageEvent(env, {