Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions migrations/0083_normalize_digest_subscription_logins.sql
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 6 additions & 1 deletion src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,12 @@ export async function upsertDigestSubscription(

export async function listDigestSubscriptionsForLogin(env: Env, login: string): Promise<DigestSubscriptionRecord[]> {
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);
}

Expand Down
35 changes: 35 additions & 0 deletions test/unit/product-usage.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { readFileSync, readdirSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { describe, expect, it } from "vitest";
import {
getContributorScoringProfile,
Expand Down Expand Up @@ -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, {
Expand Down
Loading