fix unique constraint on relational session data. Closes #4183

This commit is contained in:
Francis Cao committed 2026-07-30 10:39:38 -07:00
1 parent 047b0554b3
commit 7c030e4c5d
4 files changed
+151 -15

No files matched your search

@@ -0,0 +1,16 @@
WITH ranked_session_data AS (
SELECT
"session_data_id",
ROW_NUMBER() OVER (
PARTITION BY "session_id", "data_key"
ORDER BY "created_at" DESC NULLS LAST, "session_data_id" DESC
) AS row_num
FROM "session_data"
)
DELETE FROM "session_data"
USING ranked_session_data
WHERE "session_data"."session_data_id" = ranked_session_data."session_data_id"
AND ranked_session_data.row_num > 1;
CREATE UNIQUE INDEX "session_data_session_id_data_key_key"
ON "session_data"("session_id", "data_key");
+1
View File
@@ -208,6 +208,7 @@ model SessionData {
@@index([sessionId])
@@index([sessionId, createdAt])
@@index([websiteId, createdAt, dataKey])
@@unique([sessionId, dataKey])
@@map("session_data")
}
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { DATA_TYPE } from '@/lib/constants';
import { relationalQuery } from './saveSessionData';
const { writeRawQueryMock } = vi.hoisted(() => ({
writeRawQueryMock: vi.fn(),
}));
vi.mock('@/lib/prisma', () => ({
default: {
writeRawQuery: writeRawQueryMock,
},
}));
describe('relationalQuery', () => {
beforeEach(() => {
writeRawQueryMock.mockReset();
writeRawQueryMock.mockResolvedValue(undefined);
});
test('writes session data with a Postgres upsert keyed by sessionId and dataKey', async () => {
const createdAt = new Date('2026-07-30T10:00:00.000Z');
await relationalQuery({
websiteId: 'website-1',
sessionId: 'session-1',
sessionData: { plan: 'pro' },
distinctId: 'distinct-1',
createdAt,
});
expect(writeRawQueryMock).toHaveBeenCalledTimes(1);
const [query, params, tag] = writeRawQueryMock.mock.calls[0];
expect(query).toContain('insert into session_data');
expect(query).toContain('on conflict (session_id, data_key)');
expect(query).toContain('do update set');
expect(query).toContain('coalesce({{createdAt}}, now())');
expect(query).toContain('created_at = coalesce({{createdAt}}, session_data.created_at)');
expect(query).toContain('{{id}}');
expect(query).toContain('{{websiteId}}');
expect(query).toContain('{{sessionId}}');
expect(query).toContain('{{dataKey}}');
expect(params).toEqual({
id: expect.any(String),
websiteId: 'website-1',
sessionId: 'session-1',
dataKey: 'plan',
stringValue: 'pro',
numberValue: null,
dateValue: null,
dataType: DATA_TYPE.string,
distinctId: 'distinct-1',
createdAt,
});
expect(tag).toBe('saveSessionData');
});
test('preserves default and existing createdAt behavior when createdAt is omitted', async () => {
await relationalQuery({
websiteId: 'website-1',
sessionId: 'session-1',
sessionData: { plan: 'pro' },
distinctId: 'distinct-1',
});
const [query, params] = writeRawQueryMock.mock.calls[0];
expect(query).toContain('coalesce({{createdAt}}, now())');
expect(query).toContain('created_at = coalesce({{createdAt}}, session_data.created_at)');
expect(params.createdAt).toBeUndefined();
});
});
+60 -15
View File
@@ -30,7 +30,7 @@ export async function relationalQuery({
distinctId,
createdAt,
}: SaveSessionDataArgs) {
const { client } = prisma;
const { writeRawQuery } = prisma;
const jsonKeys = flattenJSON(sessionData);
const normalizedDistinctId = truncateString(distinctId, FIELD_LENGTH.distinctId);
@@ -49,24 +49,69 @@ export async function relationalQuery({
}));
for (const data of flattenedData) {
const { sessionId, dataKey, ...props } = data;
const {
id,
websiteId,
sessionId,
dataKey,
stringValue,
numberValue,
dateValue,
dataType,
distinctId,
createdAt,
} = data;
const updateResult = await client.sessionData.updateMany({
where: {
await writeRawQuery(
`
insert into session_data (
session_data_id,
website_id,
session_id,
data_key,
string_value,
number_value,
date_value,
data_type,
distinct_id,
created_at
)
values (
{{id}},
{{websiteId}},
{{sessionId}},
{{dataKey}},
{{stringValue}},
{{numberValue}},
{{dateValue}},
{{dataType}},
{{distinctId}},
coalesce({{createdAt}}, now())
)
on conflict (session_id, data_key)
do update set
website_id = excluded.website_id,
string_value = excluded.string_value,
number_value = excluded.number_value,
date_value = excluded.date_value,
data_type = excluded.data_type,
distinct_id = excluded.distinct_id,
created_at = coalesce({{createdAt}}, session_data.created_at)
`,
{
id,
websiteId,
sessionId,
dataKey,
stringValue,
numberValue,
dateValue,
dataType,
distinctId,
createdAt,
},
data: {
...props,
},
});
// If no record was updated, create a new one
if (updateResult.count === 0) {
await client.sessionData.create({
data,
});
}
'saveSessionData',
);
}
}