Include @mentioned names in search results

This commit is contained in:
trevor-signal 2023-06-26 14:25:48 -04:00 committed by GitHub
parent e3c6b4d9b1
commit 9c6fb29edb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1052 additions and 126 deletions

View file

@ -15,7 +15,9 @@ import {
} from '../sql/Server';
import { ReadStatus } from '../messages/MessageReadStatus';
import { SeenStatus } from '../MessageSeenStatus';
import { sql } from '../sql/util';
import { objectToJSON, sql, sqlJoin } from '../sql/util';
import type { MessageType } from '../sql/Interface';
import { BodyRange } from '../types/BodyRange';
const OUR_UUID = generateGuid();
@ -3186,4 +3188,292 @@ describe('SQL migrations test', () => {
);
});
});
describe('updateToSchemaVersion84', () => {
const schemaVersion = 84;
function composeMessage({
id,
mentions,
boldRanges,
}: {
id?: string;
mentions?: Array<string>;
boldRanges?: Array<Array<number>>;
}) {
const json: Partial<MessageType> = {
id: id ?? generateGuid(),
body: `Message body: ${id}`,
};
if (mentions) {
json.bodyRanges = mentions.map((mentionUuid, mentionIdx) => ({
start: mentionIdx,
length: 1,
mentionUuid,
}));
}
// Add some other body ranges in that are not mentions
if (boldRanges) {
json.bodyRanges = (json.bodyRanges ?? []).concat(
boldRanges.map(([start, length]) => ({
start,
length,
style: BodyRange.Style.BOLD,
}))
);
}
return json;
}
function addMessages(
messages: Array<{
mentions?: Array<string>;
boldRanges?: Array<Array<number>>;
}>
) {
const formattedMessages = messages.map(composeMessage);
db.exec(
`
INSERT INTO messages
(id, json)
VALUES
${formattedMessages
.map(message => `('${message.id}', '${objectToJSON(message)}')`)
.join(', ')};
`
);
assert.equal(
db.prepare('SELECT COUNT(*) FROM messages;').pluck().get(),
messages.length
);
return { formattedMessages };
}
function getMentions() {
return db
.prepare('SELECT messageId, mentionUuid, start, length FROM mentions;')
.all();
}
it('Creates and populates the mentions table with existing mentions', () => {
updateToVersion(schemaVersion - 1);
const userIds = new Array(5).fill(undefined).map(() => generateGuid());
const { formattedMessages } = addMessages([
{ mentions: [userIds[0]] },
{ mentions: [userIds[1]], boldRanges: [[1, 1]] },
{ mentions: [userIds[1], userIds[2]] },
{},
{ boldRanges: [[1, 4]] },
]);
// now create mentions table
updateToVersion(schemaVersion);
// only the 4 mentions should be included, with multiple rows for multiple mentions
// in a message
const mentions = getMentions();
assert.equal(mentions.length, 4);
assert.sameDeepMembers(mentions, [
{
messageId: formattedMessages[0].id,
mentionUuid: userIds[0],
start: 0,
length: 1,
},
{
messageId: formattedMessages[1].id,
mentionUuid: userIds[1],
start: 0,
length: 1,
},
{
messageId: formattedMessages[2].id,
mentionUuid: userIds[1],
start: 0,
length: 1,
},
{
messageId: formattedMessages[2].id,
mentionUuid: userIds[2],
start: 1,
length: 1,
},
]);
});
it('Updates mention table when new messages are added', () => {
updateToVersion(schemaVersion);
assert.equal(
db.prepare('SELECT COUNT(*) FROM mentions;').pluck().get(),
0
);
const userIds = new Array(5).fill(undefined).map(() => generateGuid());
const { formattedMessages } = addMessages([
{ mentions: [userIds[0]] },
{ mentions: [userIds[1]], boldRanges: [[1, 1]] },
{ mentions: [userIds[1], userIds[2]] },
{},
{ boldRanges: [[1, 4]] },
]);
// the 4 mentions should be included, with multiple rows for multiple mentions in a
// message
const mentions = getMentions();
assert.sameDeepMembers(mentions, [
{
messageId: formattedMessages[0].id,
mentionUuid: userIds[0],
start: 0,
length: 1,
},
{
messageId: formattedMessages[1].id,
mentionUuid: userIds[1],
start: 0,
length: 1,
},
{
messageId: formattedMessages[2].id,
mentionUuid: userIds[1],
start: 0,
length: 1,
},
{
messageId: formattedMessages[2].id,
mentionUuid: userIds[2],
start: 1,
length: 1,
},
]);
});
it('Removes mentions when messages are deleted', () => {
updateToVersion(schemaVersion);
assert.equal(
db.prepare('SELECT COUNT(*) FROM mentions;').pluck().get(),
0
);
const userIds = new Array(5).fill(undefined).map(() => generateGuid());
const { formattedMessages } = addMessages([
{ mentions: [userIds[0]] },
{ mentions: [userIds[1], userIds[2]], boldRanges: [[1, 1]] },
]);
assert.equal(getMentions().length, 3);
// The foreign key ON DELETE CASCADE relationship should delete mentions when the
// referenced message is deleted
db.exec(`DELETE FROM messages WHERE id = '${formattedMessages[1].id}';`);
const mentions = getMentions();
assert.equal(getMentions().length, 1);
assert.sameDeepMembers(mentions, [
{
messageId: formattedMessages[0].id,
mentionUuid: userIds[0],
start: 0,
length: 1,
},
]);
});
it('Updates mentions when messages are updated', () => {
updateToVersion(schemaVersion);
assert.equal(
db.prepare('SELECT COUNT(*) FROM mentions;').pluck().get(),
0
);
const userIds = new Array(5).fill(undefined).map(() => generateGuid());
const { formattedMessages } = addMessages([{ mentions: [userIds[0]] }]);
assert.equal(getMentions().length, 1);
// update it with 0 mentions
db.prepare(
`UPDATE messages SET json = $json WHERE id = '${formattedMessages[0].id}';`
).run({
json: objectToJSON(composeMessage({ id: formattedMessages[0].id })),
});
assert.equal(getMentions().length, 0);
// update it with a bold bodyrange
db.prepare(
`UPDATE messages SET json = $json WHERE id = '${formattedMessages[0].id}';`
).run({
json: objectToJSON(
composeMessage({ id: formattedMessages[0].id, boldRanges: [[1, 2]] })
),
});
assert.equal(getMentions().length, 0);
// update it with a three new mentions
db.prepare(
`UPDATE messages SET json = $json WHERE id = '${formattedMessages[0].id}';`
).run({
json: objectToJSON(
composeMessage({
id: formattedMessages[0].id,
mentions: [userIds[2], userIds[3], userIds[4]],
boldRanges: [[1, 2]],
})
),
});
assert.sameDeepMembers(getMentions(), [
{
messageId: formattedMessages[0].id,
mentionUuid: userIds[2],
start: 0,
length: 1,
},
{
messageId: formattedMessages[0].id,
mentionUuid: userIds[3],
start: 1,
length: 1,
},
{
messageId: formattedMessages[0].id,
mentionUuid: userIds[4],
start: 2,
length: 1,
},
]);
});
it('uses the mentionUuid index for searching mentions', () => {
updateToVersion(schemaVersion);
const [query, params] = sql`
EXPLAIN QUERY PLAN
SELECT
messages.rowid,
mentionUuid
FROM mentions
INNER JOIN messages
ON
messages.id = mentions.messageId
AND mentions.mentionUuid IN (
${sqlJoin(['a', 'b', 'c'], ', ')}
)
AND messages.isViewOnce IS NOT 1
AND messages.storyId IS NULL
LIMIT 100;
`;
const { detail } = db.prepare(query).get(params);
assert.notInclude(detail, 'B-TREE');
assert.notInclude(detail, 'SCAN');
assert.include(
detail,
'SEARCH mentions USING INDEX mentions_uuid (mentionUuid=?)'
);
});
});
});