Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation

Check warning on line 1 in desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift is 3238 lines; consider splitting files over 800 lines.

Check warning on line 1 in desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift is 3244 lines; consider splitting files over 800 lines.
import GRDB
import OmiSupport

Expand Down Expand Up @@ -167,15 +167,22 @@
try installActionItemsFTS(in: db, populateExistingRows: true)
}

/// Single source of truth for the action_items FTS5 virtual-table shape, so the install path
/// and the shadow-table repair template stay byte-identical. If the tokenizer, columns, or
/// content mapping ever diverged, recovery would build a mismatched shadow table.
private static func actionItemsFTSCreateSQL(tableName: String) -> String {
"""
CREATE VIRTUAL TABLE \(tableName) USING fts5(
description,
content='action_items',
content_rowid='id',
tokenize='unicode61'
)
"""
}

private static func installActionItemsFTS(in db: Database, populateExistingRows: Bool) throws {
try db.execute(sql: """
CREATE VIRTUAL TABLE action_items_fts USING fts5(
description,
content='action_items',
content_rowid='id',
tokenize='unicode61'
)
""")
try db.execute(sql: actionItemsFTSCreateSQL(tableName: "action_items_fts"))

try db.execute(sql: """
CREATE TRIGGER action_items_fts_ai AFTER INSERT ON action_items BEGIN
Expand Down Expand Up @@ -215,25 +222,37 @@
do {
try db.execute(sql: "DROP TABLE IF EXISTS action_items_fts")
} catch {
try forceRemoveBrokenActionItemsFTSMetadata(in: db)
try restoreMissingActionItemsFTSShadowTables(in: db)
try db.execute(sql: "DROP TABLE IF EXISTS action_items_fts")
}
}

private static func forceRemoveBrokenActionItemsFTSMetadata(in db: Database) throws {
try db.execute(sql: "PRAGMA writable_schema = ON")
defer { try? db.execute(sql: "PRAGMA writable_schema = OFF") }
private static func restoreMissingActionItemsFTSShadowTables(in db: Database) throws {
let templateName = "action_items_fts_repair_template"
try db.execute(sql: "DROP TABLE IF EXISTS \(templateName)")
try db.execute(sql: actionItemsFTSCreateSQL(tableName: templateName))
defer { try? db.execute(sql: "DROP TABLE IF EXISTS \(templateName)") }

try db.execute(sql: "DELETE FROM sqlite_master WHERE type = 'table' AND name = 'action_items_fts'")
for table in actionItemsFTSShadowTables {
do {
try db.execute(sql: "DROP TABLE IF EXISTS \(table)")
} catch {
try db.execute(sql: "DELETE FROM sqlite_master WHERE type = 'table' AND name = '\(table)'")
let exists = (try Int.fetchOne(
db,
sql: "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?",
arguments: [table]
) ?? 0) > 0
guard !exists else { continue }

let templateTable = table.replacingOccurrences(of: "action_items_fts", with: templateName)
guard let templateSQL = try String.fetchOne(
db,
sql: "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
arguments: [templateTable]
) else {
continue
}
}

let schemaVersion = (try Int.fetchOne(db, sql: "PRAGMA schema_version")) ?? 0
try db.execute(sql: "PRAGMA schema_version = \(schemaVersion + 1)")
let createSQL = templateSQL.replacingOccurrences(of: templateTable, with: table)
try db.execute(sql: createSQL)
}
}

/// Configure the database for a specific user.
Expand Down Expand Up @@ -969,7 +988,7 @@

// MARK: - Migrations

private func migrate(_ queue: DatabasePool) throws {

Check warning on line 991 in desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

private func migrate(_ queue: DatabasePool) throws is 1386 lines; consider extracting focused helpers over 150 lines.

Check warning on line 991 in desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

private func migrate(_ queue: DatabasePool) throws is 1386 lines; consider extracting focused helpers over 150 lines.
var migrator = DatabaseMigrator()

// Migration 1: Create screenshots table
Expand Down
39 changes: 30 additions & 9 deletions desktop/macos/Desktop/Tests/ActionItemsFTSRepairTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,42 @@ final class ActionItemsFTSRepairTests: XCTestCase {
XCTAssertEqual(ftsDescriptions, durableDescriptions)
}

func testRepairToleratesMissingActionItemsFTSShadowTable() async throws {
func testRepairRebuildsActionItemsFTSFromDurableRows() async throws {
let existing = try await ActionItemStorage.shared.insertLocalActionItem(
ActionItemRecord(description: "shadow table durable row", source: "test"))
XCTAssertNotNil(existing.id)
let existingId = try XCTUnwrap(existing.id)

guard let dbQueue = await RewindDatabase.shared.getDatabaseQueue() else {
return XCTFail("database should be initialized")
}

try await dbQueue.write { db in
try db.execute(sql: "PRAGMA writable_schema = ON")
let schemaVersion = (try Int.fetchOne(db, sql: "PRAGMA schema_version")) ?? 0
defer { try? db.execute(sql: "PRAGMA writable_schema = OFF") }
try db.execute(sql: "DELETE FROM sqlite_master WHERE type = 'table' AND name = 'action_items_fts_data'")
try db.execute(sql: "PRAGMA schema_version = \(schemaVersion + 1)")
try db.execute(
sql: """
INSERT INTO action_items_fts(action_items_fts, rowid, description)
VALUES ('delete', ?, ?)
""",
arguments: [existingId, existing.description])
}

try await RewindDatabase.shared.repairActionItemsFTS(in: dbQueue, reason: "missing shadow table test")
let missingMatches = try await dbQueue.read { db in
try String.fetchAll(
db,
sql: """
SELECT action_items.description
FROM action_items_fts
JOIN action_items ON action_items_fts.rowid = action_items.id
WHERE action_items_fts MATCH 'shadow'
ORDER BY action_items.id
""")
}
XCTAssertEqual(missingMatches, [])

try await RewindDatabase.shared.repairActionItemsFTS(in: dbQueue, reason: "fts rebuild test")

let afterRepair = try await ActionItemStorage.shared.insertLocalActionItem(
ActionItemRecord(description: "shadow table inserted after repair", source: "test"))
XCTAssertNotNil(afterRepair.id)

let matches = try await dbQueue.read { db in
try String.fetchAll(
Expand All @@ -103,6 +121,9 @@ final class ActionItemsFTSRepairTests: XCTestCase {
ORDER BY action_items.id
""")
}
XCTAssertEqual(matches, ["shadow table durable row"])
XCTAssertEqual(matches, [
"shadow table durable row",
"shadow table inserted after repair"
])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"change": "Repaired action item search recovery so broken FTS shadow tables are rebuilt without writing directly to SQLite catalog tables"
}
1 change: 1 addition & 0 deletions desktop/macos/e2e/flows/tasks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ covers:
- desktop/macos/Desktop/Sources/MainWindow/Pages/TasksPage.swift
- desktop/macos/Desktop/Sources/MainWindow/Components/TaskChatPanel.swift
- desktop/macos/Desktop/Sources/Stores/TasksStore.swift
- desktop/macos/Desktop/Sources/Rewind/Core/RewindDatabase.swift
- desktop/macos/Desktop/Sources/Services/RecurringTaskScheduler.swift
- desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/TaskAgent/TaskChatState.swift
- desktop/macos/Desktop/Sources/ProactiveAssistants/Core/GeminiClient.swift
Expand Down
8 changes: 1 addition & 7 deletions desktop/macos/scripts/swift-test-skips.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"max_skip_count": 5,
"max_skip_count": 4,
"allowed_tests": [
"ChatDiscoverabilityTests/testAgentControlCapabilitiesMatchCanonicalManifest",
"ChatDiscoverabilityTests/testDesktopCapabilitiesExistInAgentToolDeclarations",
"APIClientRoutingTests/testDeleteConversationRoutesToPython",
"ActionItemsFTSRepairTests/testRepairToleratesMissingActionItemsFTSShadowTable",
"PiMonoWiringTests/testLocalAgentProviderDetectorMissingPromptIsUserFacing"
],
"skips": [
Expand All @@ -23,11 +22,6 @@
"issue": "https://github.com/BasedHardware/omi/issues/9031",
"reason": "The client omits ?cascade=true; cascade is backend-gated behind owner sign-off, so flipping it is a data-deletion behavior change."
},
{
"test": "ActionItemsFTSRepairTests/testRepairToleratesMissingActionItemsFTSShadowTable",
"issue": "https://github.com/BasedHardware/omi/issues/9032",
"reason": "macOS 26 system SQLite rejects DELETE FROM sqlite_master even with writable_schema=ON, blocking the test corruption setup."
},
{
"test": "PiMonoWiringTests/testLocalAgentProviderDetectorMissingPromptIsUserFacing",
"issue": "https://github.com/BasedHardware/omi/issues/9033",
Expand Down
Loading