core: sort & group reminders in sqlite

This commit is contained in:
Abdullah Atta
2024-03-13 08:59:23 +05:00
parent 00ecb149df
commit 67fa1d6bb4
8 changed files with 431 additions and 96 deletions

View File

@@ -29,6 +29,7 @@ import { Reminder } from "../types";
import Database from "../api";
import { SQLCollection } from "../database/sql-collection";
import { isFalse } from "../database";
import { sql } from "kysely";
dayjs.extend(isTomorrow);
dayjs.extend(isSameOrBefore);
@@ -66,7 +67,7 @@ export class Reminders implements ICollection {
};
if (!reminder.date || !reminder.title)
throw new Error("date and title are required in a reminder.");
throw new Error(`date and title are required in a reminder.`);
await this.collection.upsert({
id,
@@ -247,15 +248,57 @@ export function getUpcomingReminder(reminders: Reminder[]) {
}
export function isReminderActive(reminder: Reminder) {
const time =
reminder.mode === "once"
? reminder.date
: getUpcomingReminderTime(reminder);
return (
!reminder.disabled &&
(reminder.mode !== "once" ||
time > Date.now() ||
(reminder.snoozeUntil && reminder.snoozeUntil > Date.now()))
reminder.date > Date.now() ||
(!!reminder.snoozeUntil && reminder.snoozeUntil > Date.now()))
);
}
export function createUpcomingReminderTimeQuery(now = "now") {
return sql`CASE
WHEN mode = 'once' THEN date / 1000
WHEN recurringMode = 'year' THEN
strftime('%s',
strftime('%Y-', date(${now})) || strftime('%m-%d%H:%M', date / 1000, 'unixepoch', 'localtime'),
IIF(datetime(strftime('%Y-', date(${now})) || strftime('%m-%d%H:%M', date / 1000, 'unixepoch', 'localtime')) <= datetime(${now}), '+1 year', '+0 year'),
'utc'
)
WHEN recurringMode = 'day' THEN
strftime('%s',
date(${now}) || time(date / 1000, 'unixepoch', 'localtime'),
IIF(datetime(date(${now}) || time(date / 1000, 'unixepoch', 'localtime')) <= datetime(${now}), '+1 day', '+0 day'),
'utc'
)
WHEN recurringMode = 'week' AND selectedDays IS NOT NULL AND json_array_length(selectedDays) > 0 THEN
CASE
WHEN CAST(strftime('%w', date(${now})) AS INTEGER) > (SELECT MAX(value) FROM json_each(selectedDays))
OR datetime(date(${now}) || time(date / 1000, 'unixepoch', 'localtime')) <= datetime(${now})
THEN
strftime('%s', datetime(date(${now}), time(date / 1000, 'unixepoch', 'localtime'), '+1 day', 'weekday ' || json_extract(selectedDays, '$[0]'), 'utc'))
ELSE
strftime('%s', datetime(date(${now}), time(date / 1000, 'unixepoch', 'localtime'), 'weekday ' || (SELECT value FROM json_each(selectedDays) WHERE CAST(strftime('%w', date(${now})) AS INTEGER) <= value), 'utc'))
END
WHEN recurringMode = 'month' AND selectedDays IS NOT NULL AND json_array_length(selectedDays) > 0 THEN
CASE
WHEN CAST(strftime('%d', date(${now})) AS INTEGER) > (SELECT MAX(value) FROM json_each(selectedDays))
OR datetime(date(${now}) || time(date / 1000, 'unixepoch', 'localtime')) <= datetime(${now})
THEN
strftime('%s', strftime('%Y-%m-', date(${now})) || printf('%02d', json_extract(selectedDays, '$[0]')) || time(date / 1000, 'unixepoch', 'localtime'), '+1 month', 'utc')
ELSE strftime('%s', strftime('%Y-%m-', date(${now})) || (SELECT printf('%02d', value) FROM json_each(selectedDays) WHERE value <= strftime('%d', date(${now}))) || time(date / 1000, 'unixepoch', 'localtime'), 'utc')
END
ELSE strftime('%s', date(${now}) || time(date / 1000, 'unixepoch', 'localtime'), 'utc')
END * 1000
`.$castTo<number>();
}
export function createIsReminderActiveQuery(now = "now") {
return sql`IIF(
(disabled IS NULL OR disabled = 0)
AND (mode != 'once'
OR datetime(date / 1000, 'unixepoch', 'localtime') > datetime(${now})
OR (snoozeUntil IS NOT NULL
AND datetime(snoozeUntil / 1000, 'unixepoch', 'localtime') > datetime(${now}))
), 1, 0)`.$castTo<boolean>();
}

View File

@@ -40,7 +40,7 @@ const DEFAULT_GROUP_OPTIONS = (key: GroupingKey) =>
sortBy:
key === "trash"
? "dateDeleted"
: key === "tags" || key === "reminders"
: key === "tags"
? "dateCreated"
: key === "reminders"
? "dueDate"

View File

@@ -40,7 +40,6 @@ import {
Attachment,
Color,
ContentItem,
GroupOptions,
HistorySession,
ItemType,
MaybeDeletedItem,
@@ -125,7 +124,6 @@ export interface DatabaseCollection<T, IsAsync extends boolean> {
get(id: string): AsyncOrSyncResult<IsAsync, T | undefined>;
put(items: (T | undefined)[]): Promise<SQLiteItem<T>[]>;
update(ids: string[], partial: Partial<T>): Promise<void>;
ids(options: GroupOptions): AsyncOrSyncResult<IsAsync, string[]>;
records(
ids: string[]
): AsyncOrSyncResult<

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupOptions, MaybeDeletedItem, isDeleted } from "../types";
import { MaybeDeletedItem, isDeleted } from "../types";
import EventManager from "../utils/event-manager";
import { DatabaseAccessor, DatabaseCollection, DatabaseSchema } from ".";
import { SQLCollection } from "./sql-collection";
@@ -126,10 +126,6 @@ export class SQLCachedCollection<
}
}
ids(_options: GroupOptions): string[] {
return Array.from(this.cache.keys());
}
records(ids: string[]): Record<string, MaybeDeletedItem<T> | undefined> {
const items: Record<string, MaybeDeletedItem<T> | undefined> = {};
for (const id of ids) {

View File

@@ -34,6 +34,7 @@ import {
isFalse
} from ".";
import {
AliasedRawBuilder,
AnyColumn,
AnyColumnWithTable,
ExpressionOrFactory,
@@ -46,6 +47,10 @@ import { VirtualizedGrouping } from "../utils/virtualized-grouping";
import { groupArray } from "../utils/grouping";
import { toChunks } from "../utils/array";
import { Sanitizer } from "./sanitizer";
import {
createIsReminderActiveQuery,
createUpcomingReminderTimeQuery
} from "../collections/reminders";
const formats = {
month: "%Y-%m",
@@ -207,19 +212,6 @@ export class SQLCollection<
}
}
async ids(sortOptions: GroupOptions): Promise<string[]> {
const ids = await this.db()
.selectFrom<keyof DatabaseSchema>(this.type)
.select("id")
.where(isFalse("deleted"))
.$if(this.type === "notes" || this.type === "notebooks", (eb) =>
eb.where(isFalse("dateDeleted"))
)
.orderBy(sortOptions.sortBy, sortOptions.sortDirection)
.execute();
return ids.map((id) => id.id);
}
async records(
ids: string[]
): Promise<Record<string, MaybeDeletedItem<T> | undefined>> {
@@ -432,26 +424,26 @@ export class FilteredSelector<T extends Item> {
const fields: Array<
| AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>
| AnyColumn<DatabaseSchema, keyof DatabaseSchema>
> = ["id", "type", options.sortBy];
| AliasedRawBuilder<number, "dueDate">
> = ["id", "type"];
if (this.type === "notes") fields.push("notes.pinned", "notes.conflicted");
else if (this.type === "notebooks") fields.push("notebooks.pinned");
else if (this.type === "attachments" && options.groupBy === "abc")
fields.push("attachments.filename");
else if (this.type === "reminders") {
else if (this.type === "reminders" || options.sortBy === "dueDate") {
fields.push(
"reminders.mode",
"reminders.date",
"reminders.recurringMode",
"reminders.selectedDays",
"reminders.snoozeUntil",
"reminders.disabled",
"reminders.snoozeUntil"
"reminders.date",
createUpcomingReminderTimeQuery().as("dueDate")
);
}
} else fields.push(options.sortBy);
return Array.from(
groupArray(
await this.filter
.$call(this.buildSortExpression(options))
.select(fields)
.$call(this.buildSortExpression(options, true))
.execute(),
options
).values()
@@ -508,7 +500,7 @@ export class FilteredSelector<T extends Item> {
private buildSortExpression(
options: GroupOptions | SortOptions,
persistent?: boolean
hasDueDate?: boolean
) {
const sortBy: Set<SortOptions["sortBy"]> = new Set();
if (isGroupOptions(options)) {
@@ -524,6 +516,11 @@ export class FilteredSelector<T extends Item> {
qb = qb.orderBy(sql`IFNULL(conflicted, 0) desc`);
if (this.type === "notes" || this.type === "notebooks")
qb = qb.orderBy(sql`IFNULL(pinned, 0) desc`);
if (this.type === "reminders")
qb = qb.orderBy(
(qb) => qb.parens(createIsReminderActiveQuery()),
"desc"
);
for (const item of sortBy) {
if (item === "title") {
@@ -538,36 +535,30 @@ export class FilteredSelector<T extends Item> {
? formats[options.groupBy]
: null;
if (!timeFormat || isSortByDate(options)) {
qb = qb.orderBy(item, options.sortDirection);
if (item === "dueDate") {
if (hasDueDate)
qb = qb.orderBy(item as any, options.sortDirection);
else
qb = qb.orderBy(
(qb) => qb.parens(createUpcomingReminderTimeQuery()),
options.sortDirection
);
} else qb = qb.orderBy(item, options.sortDirection);
continue;
}
qb = qb.orderBy(
sql`strftime('${sql.raw(timeFormat)}', ${sql.raw(
item
)} / 1000, 'unixepoch')`,
)} / 1000, 'unixepoch', 'localtime')`,
options.sortDirection
);
}
}
if (persistent) qb = qb.orderBy("id asc");
return qb;
};
}
private sortFields(options: SortOptions, persistent?: boolean) {
const fields: Array<
| AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>
| AnyColumn<DatabaseSchema, keyof DatabaseSchema>
> = [];
if (this.type === "notes") fields.push("conflicted");
if (this.type === "notes" || this.type === "notebooks")
fields.push("pinned");
fields.push(options.sortBy);
if (persistent) fields.push("id");
return fields;
}
}
function isGroupOptions(
@@ -582,6 +573,7 @@ function isSortByDate(options: SortOptions | GroupOptions) {
options.sortBy === "dateEdited" ||
options.sortBy === "dateDeleted" ||
options.sortBy === "dateModified" ||
options.sortBy === "dateUploaded"
options.sortBy === "dateUploaded" ||
options.sortBy === "dueDate"
);
}

View File

@@ -45,7 +45,6 @@ export type GroupingKey =
| "notes"
| "notebooks"
| "tags"
//| "topics"
| "trash"
| "favorites"
| "reminders";