From 9d84b2f42074c9ed5060e022121ad55372c75828 Mon Sep 17 00:00:00 2001 From: Daniel Gasienica Date: Fri, 13 Apr 2018 21:47:06 -0400 Subject: [PATCH] Index messages with attachments using a boolean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When indexing message attachment metadata using numeric indexes such as: ```javascript { conversationId: '+12223334455', received_at: 123, attachments: […], numAttachments: 2, }, { conversationId: '+12223334455', received_at: 456, attachments: [], numAttachments: 0, } { conversationId: '+12223334455', received_at: 789, attachments: [], numAttachments: 1, } ``` It creates an index as follows: ``` [conversationId, received_at, numAttachments] ['+12223334455', 123, 2] ['+12223334455', 456, 0] ['+12223334455', 789, 1] ``` This means a query such as… ``` lowerBound: ['+12223334455', 0, 1 ] upperBound: ['+12223334455', Number.MAX_VALUE, Number.MAX_VALUE] ``` …will return all three original entries because they span the `received_at` from `0` through `Number.MAX_VALUE`. One workaround is to index booleans using `1 | undefined` where `1` is included in the index and `undefined` is not, but that way we lose the ability to query for the `false` value. Instead, we flip adjust the index to `[conversationId, hasAttachments, received_at]` and can then query messages with attachments using ``` [conversationId, 1 /* hasAttachments */, 0 /* received_at */] [conversationId, 1 /* hasAttachments */, Number.MAX_VALUE /* received_at */] ``` --- js/modules/migrations/18/index.js | 8 ++++---- js/modules/types/message.js | 6 +++--- test/modules/types/message_test.js | 6 +++--- .../initializeAttachmentMetadata_test.ts | 6 +++--- ts/types/IndexedDB.ts | 12 ++++++++++++ ts/types/Message.ts | 11 ++++++----- .../message/initializeAttachmentMetadata.ts | 19 +++++++++++-------- 7 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 ts/types/IndexedDB.ts diff --git a/js/modules/migrations/18/index.js b/js/modules/migrations/18/index.js index a1072f227..c71747461 100644 --- a/js/modules/migrations/18/index.js +++ b/js/modules/migrations/18/index.js @@ -2,14 +2,14 @@ exports.run = (transaction) => { const messagesStore = transaction.objectStore('messages'); [ - 'numAttachments', - 'numVisualMediaAttachments', - 'numFileAttachments', + 'hasAttachments', + 'hasVisualMediaAttachments', + 'hasFileAttachments', ].forEach((name) => { console.log(`Create message attachment metadata index: '${name}'`); messagesStore.createIndex( name, - ['conversationId', 'received_at', name], + ['conversationId', name, 'received_at'], { unique: false } ); }); diff --git a/js/modules/types/message.js b/js/modules/types/message.js index 0e8c63978..641582266 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -24,9 +24,9 @@ const PRIVATE = 'private'; // - Quotes: Write thumbnail data to disk and store relative path to it. // Version 5 // - Attachments: Track number and kind of attachments for media gallery -// - `numAttachments: Number` -// - `numVisualMediaAttachments: Number` (for media gallery ‘Media’ view) -// - `numFileAttachments: Number` (for media gallery ‘Documents’ view) +// - `hasAttachments?: 1 | undefined` +// - `hasVisualMediaAttachments?: 1 | undefined` (for media gallery ‘Media’ view) +// - `hasFileAttachments: ?1 | undefined` (for media gallery ‘Documents’ view) const INITIAL_SCHEMA_VERSION = 0; diff --git a/test/modules/types/message_test.js b/test/modules/types/message_test.js index 42e8c4c6c..15f285813 100644 --- a/test/modules/types/message_test.js +++ b/test/modules/types/message_test.js @@ -181,9 +181,9 @@ describe('Message', () => { fileName: 'test\uFFFDfig.exe', size: 1111, }], - numAttachments: 1, - numVisualMediaAttachments: 0, - numFileAttachments: 1, + hasAttachments: 1, + hasVisualMediaAttachments: undefined, + hasFileAttachments: 1, schemaVersion: Message.CURRENT_SCHEMA_VERSION, }; diff --git a/ts/test/types/message/initializeAttachmentMetadata_test.ts b/ts/test/types/message/initializeAttachmentMetadata_test.ts index c6b69d198..2979f13f9 100644 --- a/ts/test/types/message/initializeAttachmentMetadata_test.ts +++ b/ts/test/types/message/initializeAttachmentMetadata_test.ts @@ -38,9 +38,9 @@ describe('Message', () => { fileName: 'foo.jpg', size: 1111, }], - numAttachments: 1, - numVisualMediaAttachments: 1, - numFileAttachments: 0, + hasAttachments: 1, + hasVisualMediaAttachments: 1, + hasFileAttachments: undefined, }; const actual = await Message.initializeAttachmentMetadata(input); diff --git a/ts/types/IndexedDB.ts b/ts/types/IndexedDB.ts new file mode 100644 index 000000000..43080c78b --- /dev/null +++ b/ts/types/IndexedDB.ts @@ -0,0 +1,12 @@ +/** + * @prettier + */ + +// IndexedDB doesn’t support boolean indexes so we map `true` to 1 and `false` +// to `0`. +// N.B. Using `undefined` allows excluding an entry from an index. Useful +// when index size is a consideration or one only needs to query for `true`. +export type IndexableBoolean = 1 | 0; + +export const toIndexableBoolean = (value: boolean): IndexableBoolean => + value ? 1 : 0; diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 2b5828edb..bb01ef089 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -2,6 +2,7 @@ * @prettier */ import { Attachment } from './Attachment'; +import { IndexableBoolean } from './IndexedDB'; export type Message = IncomingMessage | OutgoingMessage | VerifiedChangeMessage; @@ -73,8 +74,8 @@ type ExpirationTimerUpdate = Readonly<{ }>; }>; -type Message4 = Readonly<{ - numAttachments?: number; - numVisualMediaAttachments?: number; - numFileAttachments?: number; -}>; +type Message4 = Partial>; diff --git a/ts/types/message/initializeAttachmentMetadata.ts b/ts/types/message/initializeAttachmentMetadata.ts index 6cbeeda7a..697390f3b 100644 --- a/ts/types/message/initializeAttachmentMetadata.ts +++ b/ts/types/message/initializeAttachmentMetadata.ts @@ -4,25 +4,28 @@ import { partition } from 'lodash'; import * as Attachment from '../Attachment'; +import * as IndexedDB from '../IndexedDB'; import { Message } from '../Message'; export const initializeAttachmentMetadata = async ( - message: Message + message: Message, ): Promise => { if (message.type === 'verified-change') { return message; } - const numAttachments = message.attachments.length; - const [numVisualMediaAttachments, numFileAttachments] = partition( + const hasAttachments = IndexedDB.toIndexableBoolean(message.attachments.length > 0); + const [hasVisualMediaAttachments, hasFileAttachments] = partition( message.attachments, - Attachment.isVisualMedia - ).map(attachments => attachments.length); + Attachment.isVisualMedia, + ) + .map((attachments) => attachments.length > 0) + .map(IndexedDB.toIndexableBoolean); return { ...message, - numAttachments, - numVisualMediaAttachments, - numFileAttachments, + hasAttachments, + hasVisualMediaAttachments, + hasFileAttachments, }; };