Skip to content

Commit 4fe8145

Browse files
authored
Add .notedb database file format (#16)
### Added - Commonplace Book now supports a new file format: .notedb, which is a single sqlite file. It is compatible with iCloud Document Storage though. - Database storage is the default for new documents. - Database storage has full-text search!
1 parent 4de8407 commit 4fe8145

37 files changed

Lines changed: 1716 additions & 386 deletions

.swiftlint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ opt_in_rules:
1919
identifier_name:
2020
excluded:
2121
- i
22+
- id
23+
- db
2224

2325
line_length: 100
2426

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Added
6+
7+
- Commonplace Book now supports a new file format: .notedb, which is a single sqlite file. It is compatible with iCloud Document Storage though.
8+
- Database storage is the default for new documents.
9+
- Database storage has full-text search!
10+
311
## [0.14.0] - 2020-01-02
412

513
### Changed

CommonplaceBookApp.xcodeproj/project.pbxproj

Lines changed: 58 additions & 14 deletions
Large diffs are not rendered by default.

CommonplaceBookApp.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 29 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CommonplaceBookApp/AppDelegate.swift

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
4949

5050
let window = UIWindow(frame: UIScreen.main.bounds)
5151

52-
let browser = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: ["org.brians-brain.notebundle"])
52+
let browser = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: ["org.brians-brain.notebundle", "org.brians-brain.notedb"])
5353
browser.delegate = self
5454

5555
window.rootViewController = browser
5656
window.makeKeyAndVisible()
5757
self.window = window
5858

59-
if let openedDocumentBookmarkData = openedDocumentBookmark {
59+
if !isUITesting, let openedDocumentBookmarkData = openedDocumentBookmark {
6060
DDLogInfo("Bookmark data exists for an open document")
6161
var isStale: Bool = false
6262
do {
@@ -78,7 +78,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
7878
makeMetadataProvider(completion: { metadataProviderResult in
7979
switch metadataProviderResult {
8080
case .success(let metadataProvider):
81-
self.openDocument(at: metadataProvider.container.appendingPathComponent("archive.notebundle"), from: viewController, animated: false)
81+
self.openDocument(at: metadataProvider.container.appendingPathComponent("archive.notedb"), from: viewController, animated: false)
8282
case .failure(let error):
8383
let messageText = "Error opening Notebook: \(error.localizedDescription)"
8484
let alertController = UIAlertController(title: "Error", message: messageText, preferredStyle: .alert)
@@ -143,8 +143,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
143143
}
144144
}
145145

146+
private lazy var isUITesting: Bool = {
147+
CommandLine.arguments.contains("--uitesting")
148+
}()
149+
146150
private func makeMetadataProvider(completion: @escaping (Result<FileMetadataProvider, Swift.Error>) -> Void) {
147-
if CommandLine.arguments.contains("--uitesting") {
151+
if isUITesting {
148152
let container = FileManager.default.temporaryDirectory.appendingPathComponent("uitesting")
149153
completion(makeDirectoryProvider(at: container, deleteExistingContents: true))
150154
return
@@ -202,10 +206,18 @@ extension AppDelegate: UIDocumentBrowserViewControllerDelegate {
202206
/// - parameter controller: The view controller from which to present the DocumentListViewController
203207
private func openDocument(at url: URL, from controller: UIDocumentBrowserViewController, animated: Bool) {
204208
DDLogInfo("Opening document at \(url)")
205-
let noteArchiveDocument = NoteDocumentStorage(
206-
fileURL: url,
207-
parsingRules: ParsingRules.commonplace
208-
)
209+
let noteArchiveDocument: NoteStorage
210+
if url.pathExtension == "notebundle" {
211+
noteArchiveDocument = NoteDocumentStorage(
212+
fileURL: url,
213+
parsingRules: ParsingRules.commonplace
214+
)
215+
} else if url.pathExtension == "notedb" {
216+
noteArchiveDocument = NoteSqliteStorage(fileURL: url, parsingRules: ParsingRules.commonplace)
217+
} else {
218+
assertionFailure("Unknown note format: \(url.pathExtension)")
219+
return
220+
}
209221
DDLogInfo("Using document at \(noteArchiveDocument.fileURL)")
210222
let documentListViewController = DocumentListViewController(notebook: noteArchiveDocument)
211223
documentListViewController.didTapFilesAction = { [weak self] in
@@ -229,11 +241,11 @@ extension AppDelegate: UIDocumentBrowserViewControllerDelegate {
229241
noteIdentifierCopy.flatMap { documentListViewController.showPage(with: $0) }
230242
let properties: [String: String] = [
231243
"Success": success.description,
232-
"documentState": String(describing: noteArchiveDocument.documentState),
233-
"previousError": noteArchiveDocument.previousError?.localizedDescription ?? "nil",
244+
// "documentState": String(describing: noteArchiveDocument.documentState),
245+
// "previousError": noteArchiveDocument.previousError?.localizedDescription ?? "nil",
234246
]
235247
DDLogInfo("In open completion handler. \(properties)")
236-
if success {
248+
if success, !self.isUITesting {
237249
self.openedDocumentBookmark = try? url.bookmarkData()
238250
}
239251
})
@@ -248,23 +260,17 @@ extension AppDelegate: UIDocumentBrowserViewControllerDelegate {
248260

249261
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: @escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
250262
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
251-
let url = directoryURL.appendingPathComponent(UUID().uuidString).appendingPathExtension("notebundle")
252-
let document = NoteDocumentStorage(fileURL: url, parsingRules: ParsingRules.commonplace)
253-
document.save(to: url, for: .forCreating) { saveSuccess in
254-
guard saveSuccess else {
255-
DDLogError("Could not save document to \(url): \(document.previousError?.localizedDescription ?? "nil")")
256-
importHandler(nil, .none)
257-
return
258-
}
259-
document.close { closeSuccess in
260-
guard closeSuccess else {
261-
DDLogError("Could not close document at \(url): \(document.previousError?.localizedDescription ?? "nil")")
262-
importHandler(nil, .none)
263-
return
264-
}
265-
importHandler(url, .copy)
266-
}
263+
let url = directoryURL.appendingPathComponent("commonplace").appendingPathExtension("notedb")
264+
do {
265+
let document = NoteSqliteStorage(fileURL: url, parsingRules: ParsingRules.commonplace)
266+
try document.open()
267+
try document.flush()
268+
} catch {
269+
DDLogError("Error creating new document: \(error)")
270+
importHandler(nil, .none)
271+
return
267272
}
273+
importHandler(url, .copy)
268274
}
269275

270276
func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) {

CommonplaceBookApp/Challenge.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import UIKit
77
/// Uniquely identifies a challenge.
88
public struct ChallengeIdentifier: Hashable {
99
/// The SHA1 digest of the template that created this challenge.
10-
public let templateDigest: String?
10+
public var templateDigest: String?
1111

1212
/// The index of this challenge in the template's challenges array.
1313
public let index: Int

CommonplaceBookApp/ChallengeTemplate.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,7 @@ open class ChallengeTemplate: Codable {
4040
/// the template before creating any challenges from it.
4141
///
4242
/// Can only be set from nil to non-nil once; immutable once set.
43-
public var templateIdentifier: String? {
44-
willSet {
45-
precondition(templateIdentifier == nil)
46-
}
47-
}
43+
public var templateIdentifier: String?
4844

4945
/// Subclasses should override and return their particular type.
5046
/// This is a computed, rather than a stored, property so it does not get serialized.

CommonplaceBookApp/DocumentListViewController.swift

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ final class DocumentListViewController: UIViewController {
4646
public let notebook: NoteStorage
4747
public var didTapFilesAction: (() -> Void)?
4848
private var dataSource: DocumentTableController?
49-
private var currentSpotlightQuery: CSSearchQuery?
5049
private var notebookSubscription: AnyCancellable?
5150

5251
private lazy var documentBrowserButton: UIBarButtonItem = {
@@ -277,16 +276,11 @@ extension DocumentListViewController: UISearchResultsUpdating, UISearchBarDelega
277276
guard searchController.isActive else {
278277
dataSource?.hashtags = []
279278
dataSource?.filteredPageIdentifiers = nil
280-
currentSpotlightQuery = nil
281279
updateStudySession()
282280
return
283281
}
284282
let pattern = searchController.searchBar.text ?? ""
285-
var queryString = """
286-
contentDescription == "*\(pattern)*"dc
287-
"""
288283
if let selectedHashtag = searchController.searchBar.searchTextField.tokens.first?.representedObject as? String {
289-
queryString.append(" && keywords == \"\(selectedHashtag)\"dc")
290284
dataSource?.hashtags = []
291285
dataSource?.filteredHashtag = selectedHashtag
292286
} else {
@@ -295,23 +289,13 @@ extension DocumentListViewController: UISearchResultsUpdating, UISearchBarDelega
295289
.filter { $0.fuzzyMatch(pattern: pattern) }
296290
dataSource?.filteredHashtag = nil
297291
}
298-
DDLogInfo("Issuing query: \(queryString)")
299-
currentSpotlightQuery?.cancel()
300-
let query = CSSearchQuery(queryString: queryString, attributes: nil)
301-
var allIdentifiers: [Note.Identifier] = []
302-
query.foundItemsHandler = { items in
303-
allIdentifiers.append(contentsOf: items.map { Note.Identifier(rawValue: $0.uniqueIdentifier) })
304-
}
305-
query.completionHandler = { _ in
306-
DDLogInfo("Found identifiers: \(allIdentifiers)")
307-
DispatchQueue.main.async {
308-
if searchController.isActive, self.currentSpotlightQuery == query {
309-
self.dataSource?.filteredPageIdentifiers = Set(allIdentifiers)
310-
}
311-
}
292+
DDLogInfo("Issuing query: \(pattern)")
293+
do {
294+
let allIdentifiers = try notebook.search(for: pattern)
295+
dataSource?.filteredPageIdentifiers = Set(allIdentifiers)
296+
} catch {
297+
DDLogError("Error issuing full text query: \(error)")
312298
}
313-
query.start()
314-
currentSpotlightQuery = query
315299
}
316300

317301
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
@@ -329,8 +313,12 @@ extension DocumentListViewController: StudyViewControllerDelegate {
329313
_ studyViewController: StudyViewController,
330314
didFinishSession session: StudySession
331315
) {
332-
notebook.updateStudySessionResults(session, on: Date())
333-
updateStudySession()
316+
do {
317+
try notebook.updateStudySessionResults(session, on: Date())
318+
updateStudySession()
319+
} catch {
320+
DDLogError("Unexpected error recording study session results: \(error)")
321+
}
334322
}
335323

336324
func studyViewControllerDidCancel(_ studyViewController: StudyViewController) {

CommonplaceBookApp/DocumentTableController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ private extension DocumentTableController {
341341
notebook.studySession(filter: nil, date: Date()) { studySession in
342342
self.cardsPerDocument = studySession
343343
.reduce(into: [Note.Identifier: Int]()) { cardsPerDocument, card in
344-
cardsPerDocument[card.properties.documentName] = cardsPerDocument[card.properties.documentName, default: 0] + 1
344+
cardsPerDocument[card.noteIdentifier] = cardsPerDocument[card.noteIdentifier, default: 0] + 1
345345
}
346346
DDLogInfo(
347347
"studySession.count = \(studySession.count). " +

CommonplaceBookApp/EditVocabularyView.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ struct EditVocabularyView: View {
7575
private var selectedImage: SwiftUI.Image? {
7676
if
7777
let key = vocabularyTemplate.imageAsset,
78-
let data = notebook.data(for: key),
78+
let data = try? notebook.data(for: key),
7979
let uiImage = UIImage(data: data) {
8080
return Image(uiImage: uiImage)
8181
} else {
@@ -87,9 +87,14 @@ struct EditVocabularyView: View {
8787
// TODO: Only store asset data when we commit this association?
8888
private func onSelectedImage(encodedImage: EncodedImage) {
8989
DDLogInfo("Selected image: \(encodedImage)")
90-
let key = notebook.storeAssetData(encodedImage.data, typeHint: encodedImage.encoding)
91-
DDLogInfo("Saved image data as asset \(key)")
92-
vocabularyTemplate.imageAsset = key
90+
let key = encodedImage.data.sha1Digest() + "." + encodedImage.encoding
91+
do {
92+
let actualKey = try notebook.storeAssetData(encodedImage.data, key: key)
93+
vocabularyTemplate.imageAsset = actualKey
94+
DDLogInfo("Saved image data as asset \(actualKey)")
95+
} catch {
96+
DDLogError("Unexpected error saving image: \(error)")
97+
}
9398
}
9499
}
95100

0 commit comments

Comments
 (0)