diff --git a/packages/core/__tests__/fts-triggers.test.ts b/packages/core/__tests__/fts-triggers.test.ts
index 4f7fbf628..6a2590d11 100644
--- a/packages/core/__tests__/fts-triggers.test.ts
+++ b/packages/core/__tests__/fts-triggers.test.ts
@@ -18,7 +18,7 @@ along with this program. If not, see .
*/
import { expect, test } from "vitest";
-import { TEST_NOTE, databaseTest, noteTest } from "./utils";
+import { databaseTest, noteTest } from "./utils";
test("updating deleted content should not throw", () =>
databaseTest().then(async (db) => {
diff --git a/packages/core/src/api/lookup.ts b/packages/core/src/api/lookup.ts
index d312ebd75..970fb6112 100644
--- a/packages/core/src/api/lookup.ts
+++ b/packages/core/src/api/lookup.ts
@@ -25,6 +25,7 @@ import { AnyColumnWithTable, Kysely, sql } from "kysely";
import { FilteredSelector } from "../database/sql-collection";
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
import { logger } from "../logger";
+import { rebuildSearchIndex } from "../database/fts";
type SearchResults = {
sorted: (limit?: number) => Promise>;
@@ -245,4 +246,9 @@ export default class Lookup {
if (!ids.length) return [];
return selector.items(ids);
}
+
+ async rebuild() {
+ const db = this.db.sql() as unknown as Kysely;
+ await rebuildSearchIndex(db);
+ }
}
diff --git a/packages/core/src/database/fts.ts b/packages/core/src/database/fts.ts
new file mode 100644
index 000000000..dc3ca49be
--- /dev/null
+++ b/packages/core/src/database/fts.ts
@@ -0,0 +1,74 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { Kysely, sql } from "kysely";
+import { RawDatabaseSchema } from ".";
+
+export async function rebuildSearchIndex(db: Kysely) {
+ await db.transaction().execute(async (tx) => {
+ for (const query of [
+ sql`INSERT INTO content_fts(content_fts) VALUES('delete-all')`,
+ sql`INSERT INTO notes_fts(notes_fts) VALUES('delete-all')`
+ ]) {
+ await query.execute(tx);
+ }
+
+ await tx
+ .insertInto("content_fts")
+ .columns(["rowid", "id", "data", "noteId"])
+ .expression((eb) =>
+ eb
+ .selectFrom("content")
+ .where((eb) =>
+ eb.and([
+ eb("noteId", "is not", null),
+ eb("data", "is not", null),
+ eb("deleted", "is not", true)
+ ])
+ )
+ .select([
+ "rowid",
+ "id",
+ sql`IIF(locked == 1, '', data)`.as("data"),
+ "noteId"
+ ])
+ )
+ .execute();
+
+ await tx
+ .insertInto("notes_fts")
+ .columns(["rowid", "id", "title"])
+ .expression((eb) =>
+ eb
+ .selectFrom("notes")
+ .where((eb) =>
+ eb.and([eb("title", "is not", null), eb("deleted", "is not", true)])
+ )
+ .select(["rowid", "id", "title"])
+ )
+ .execute();
+
+ for (const query of [
+ sql`INSERT INTO content_fts(content_fts) VALUES('optimize')`,
+ sql`INSERT INTO notes_fts(notes_fts) VALUES('optimize')`
+ ]) {
+ await query.execute(tx);
+ }
+ });
+}
diff --git a/packages/core/src/database/migrations.ts b/packages/core/src/database/migrations.ts
index d06d4fecc..12fa7a161 100644
--- a/packages/core/src/database/migrations.ts
+++ b/packages/core/src/database/migrations.ts
@@ -24,6 +24,7 @@ import {
MigrationProvider,
sql
} from "kysely";
+import { rebuildSearchIndex } from "./fts";
const COLLATE_NOCASE: ColumnBuilderCallback = (col) =>
col.modifyEnd(sql`collate nocase`);
@@ -283,6 +284,11 @@ export class NNMigrationProvider implements MigrationProvider {
.execute();
},
async down(db) {}
+ },
+ "2": {
+ async up(db) {
+ await rebuildSearchIndex(db);
+ }
}
};
}
diff --git a/packages/core/src/database/triggers.ts b/packages/core/src/database/triggers.ts
index 295da9734..47222f554 100644
--- a/packages/core/src/database/triggers.ts
+++ b/packages/core/src/database/triggers.ts
@@ -31,16 +31,16 @@ export async function createTriggers(db: Kysely) {
.addEvent("insert")
.when((eb) =>
eb.and([
- eb("new.deleted", "is not", true),
- eb("new.locked", "is not", true),
- eb("new.data", "is not", null)
+ eb("new.noteId", "is not", null),
+ eb("new.data", "is not", null),
+ eb("new.deleted", "is not", true)
])
)
.addQuery((c) =>
c.insertInto("content_fts").values({
rowid: sql`new.rowid`,
id: sql`new.id`,
- data: sql`new.data`,
+ data: sql`IIF(new.locked == 1, '', new.data)`,
noteId: sql`new.noteId`
})
)
@@ -53,6 +53,13 @@ export async function createTriggers(db: Kysely) {
.onTable("content", "main")
.after()
.addEvent("delete")
+ .when((eb) =>
+ eb.and([
+ eb("old.noteId", "is not", null),
+ eb("old.data", "is not", null),
+ eb("old.deleted", "is not", true)
+ ])
+ )
.addQuery((c) =>
c.insertInto("content_fts").values({
content_fts: sql.lit("delete"),
@@ -73,9 +80,9 @@ export async function createTriggers(db: Kysely) {
.addEvent("update")
.when((eb) =>
eb.and([
- eb("old.deleted", "is not", true),
eb("old.noteId", "is not", null),
- eb("old.data", "is not", null)
+ eb("old.data", "is not", null),
+ eb("old.deleted", "is not", true)
])
)
.addQuery((c) =>
@@ -107,7 +114,8 @@ export async function createTriggers(db: Kysely) {
.addEvent("insert")
.when((eb) =>
eb.and([
- eb.or([eb("new.deleted", "is", null), eb("new.deleted", "==", false)])
+ eb("new.title", "is not", null),
+ eb("new.deleted", "is not", true)
])
)
.addQuery((c) =>
@@ -126,6 +134,12 @@ export async function createTriggers(db: Kysely) {
.onTable("notes", "main")
.after()
.addEvent("delete")
+ .when((eb) =>
+ eb.and([
+ eb("old.title", "is not", null),
+ eb("old.deleted", "is not", true)
+ ])
+ )
.addQuery((c) =>
c.insertInto("notes_fts").values({
notes_fts: sql.lit("delete"),