2

In a document-based SwiftUI application, I’d like to persist each document to a separate Sqlite file using GRDB as a Sqlite wrapper. It’s straightforward to load Sqlite files in a document that implements the FileDocument protocol by creating a DatabaseQueue for the file to be loaded and using its .backup(to:) method to copy to an in-memory DatabaseQueue. How should I implement saving in the func fileWrapper(configuration: WriteConfiguration) method? There doesn’t seem to be an obvious way to use the same .backup(to:) approach.

I found an example application by Andre Yonadam that approaches this in the same way in a subclass of NSDocument:

override func write(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, originalContentsURL absoluteOriginalContentsURL: URL?) throws {
    let destination = try DatabaseQueue(path: url.path)
    do {
        try memoryDBQueue.backup(to: destination)
    } catch {
        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }
}

override func read(from url: URL, ofType typeName: String) throws {
    let source = try DatabaseQueue(path: url.path)
    do {
        try source.backup(to: memoryDBQueue)
    } catch {
        throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }
}
Rich
  • 3,095
  • 17
  • 17

1 Answers1

2

It’s perhaps not the cleanest solution but I worked around this by implementing a subclass of FileWrapper that knows how to write to Sqlite files:

class SqliteFileWrapper: FileWrapper {

    var databaseQueue: DatabaseQueue

    init (fromDatabaseQueue databaseQueue: DatabaseQueue) {
        self.databaseQueue = databaseQueue
        super.init(regularFileWithContents: "".data(using: .utf8)!)
    }

    required init?(coder inCoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func write(
        to url: URL,
        options: FileWrapper.WritingOptions = [],
        originalContentsURL: URL?
    ) throws {
        let destination = try DatabaseQueue(path: url.path)
        do {
            try databaseQueue.backup(to: destination)
        } catch {
            throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
        }
    }

}

and then in my FileDocument subclass I create a SqliteFileWrapper instead of a FileWrapper:

func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
    SqliteFileWrapper(fromDatabaseQueue: memoryDBQueue)
}
Rich
  • 3,095
  • 17
  • 17
  • Is this safe to do? From the docs: "Serialization and deserialization occur on a background thread." The file wrapper contents are handed over to a background thread which then does the actual writing to disk. Could the system not just write the contents to a temp location and then move it to its final destination. In that case, it is not be save to use with database files. – user965972 Nov 02 '21 at 13:34
  • 1
    With the proviso that I’m not an expert, I think it should be safe because it’s just doing a backup of an in-memory database to the file location on write and a restore to a fresh in-memory database on read. It’s not opening the file as a live database. – Rich Nov 03 '21 at 14:48
  • Ok. Yes that would be save. More care is needed for a live database. – user965972 Nov 03 '21 at 15:17