Make RSTree and RSDatabase local packages.

This commit is contained in:
Brent Simmons 2024-11-08 21:52:53 -08:00
parent fe728c72b3
commit 7e2c668974
54 changed files with 7748 additions and 136 deletions

View File

@ -55,7 +55,6 @@
3B826DCB2385C84800FC1ADB /* AccountsFeedWrangler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */; };
3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */; };
4679674625E599C100844E8D /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 4679674525E599C100844E8D /* Articles */; };
4679674725E599C100844E8D /* Articles in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 4679674525E599C100844E8D /* Articles */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
49F40DF82335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
49F40DF92335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
510289CD24519A1D00426DDF /* SelectComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510289CC24519A1D00426DDF /* SelectComboTableViewCell.swift */; };
@ -129,10 +128,6 @@
5137C2E626F3F52D009EFEDB /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */; };
5137C2EA26F63AE6009EFEDB /* ArticleThemeImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */; };
51386A8E25673277005F3762 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51386A8D25673276005F3762 /* AccountCell.swift */; };
5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; };
5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94B24D3417A00AFF0FE /* RSDatabase */; };
5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94B24D3417A00AFF0FE /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5138E95224D3418100AFF0FE /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; };
5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513C5CE8232571C2003D4054 /* ShareViewController.swift */; };
@ -163,8 +158,6 @@
5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5148F4542336DB7000F8CD8B /* MasterTimelineTitleView.swift */; };
514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; };
514C16CE24D2E63F009A3AFA /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16CD24D2E63F009A3AFA /* Account */; };
514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; };
514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; };
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
@ -216,8 +209,6 @@
51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; };
51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; };
51A66685238075AE00CB272D /* AddFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddFeedDefaultContainer.swift */; };
51A737BF24DB197F0015FA66 /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737BE24DB197F0015FA66 /* RSDatabase */; };
51A737C024DB197F0015FA66 /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737BE24DB197F0015FA66 /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51A737C824DB19CC0015FA66 /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C724DB19CC0015FA66 /* RSParser */; };
51A737C924DB19CC0015FA66 /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C724DB19CC0015FA66 /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; };
@ -248,10 +239,6 @@
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
51BC2F3824D3439A00E90810 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F3724D3439A00E90810 /* Account */; };
51BC2F4824D3439E00E90810 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4724D3439E00E90810 /* RSTree */; };
51BC2F4924D3439E00E90810 /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4724D3439E00E90810 /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4C24D343AB00E90810 /* RSTree */; };
51BC2F4E24D343AB00E90810 /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4C24D343AB00E90810 /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
51BC4B01247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; };
51C03081257D815A00609262 /* UnifiedWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C0307F257D815A00609262 /* UnifiedWindow.storyboard */; };
@ -392,15 +379,10 @@
841387782CD897C500E8490F /* RSCoreObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 841387772CD897C500E8490F /* RSCoreObjC */; };
841387792CD897C500E8490F /* RSCoreObjC in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 841387772CD897C500E8490F /* RSCoreObjC */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8413877B2CD897CF00E8490F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 8413877A2CD897CF00E8490F /* RSCore */; };
8413877C2CD897CF00E8490F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 8413877A2CD897CF00E8490F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8413877E2CD897CF00E8490F /* RSCoreObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 8413877D2CD897CF00E8490F /* RSCoreObjC */; };
8413877F2CD897CF00E8490F /* RSCoreObjC in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 8413877D2CD897CF00E8490F /* RSCoreObjC */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
841387812CD897EF00E8490F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 841387802CD897EF00E8490F /* RSCore */; };
841387822CD897EF00E8490F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 841387802CD897EF00E8490F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
841387842CD897EF00E8490F /* RSCoreObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 841387832CD897EF00E8490F /* RSCoreObjC */; };
841387852CD897EF00E8490F /* RSCoreObjC in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 841387832CD897EF00E8490F /* RSCoreObjC */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
841387872CD89E5200E8490F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 841387862CD89E5200E8490F /* RSCore */; };
841387882CD89E5200E8490F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 841387862CD89E5200E8490F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8413878A2CD8A38A00E8490F /* UniformTypeIdentifiers+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841387892CD8A38A00E8490F /* UniformTypeIdentifiers+Extras.swift */; };
8413878B2CD8A38A00E8490F /* UniformTypeIdentifiers+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841387892CD8A38A00E8490F /* UniformTypeIdentifiers+Extras.swift */; };
8413878E2CDC790C00E8490F /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 8413878D2CDC790C00E8490F /* RSWeb */; };
@ -408,6 +390,14 @@
841387912CDC791B00E8490F /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 841387902CDC791B00E8490F /* RSWeb */; };
841387922CDC791B00E8490F /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 841387902CDC791B00E8490F /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */; };
8417FA402CDF2E4B005F989B /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA3F2CDF2E4B005F989B /* RSDatabase */; };
8417FA412CDF2E4B005F989B /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA3F2CDF2E4B005F989B /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8417FA432CDF2E56005F989B /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA422CDF2E56005F989B /* RSDatabase */; };
8417FA442CDF2E56005F989B /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA422CDF2E56005F989B /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8417FA462CDF2E7E005F989B /* RSDatabaseObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA452CDF2E7E005F989B /* RSDatabaseObjC */; };
8417FA472CDF2E7E005F989B /* RSDatabaseObjC in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA452CDF2E7E005F989B /* RSDatabaseObjC */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
8417FA492CDF2E8A005F989B /* RSDatabaseObjC in Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA482CDF2E8A005F989B /* RSDatabaseObjC */; };
8417FA4A2CDF2E8A005F989B /* RSDatabaseObjC in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 8417FA482CDF2E8A005F989B /* RSDatabaseObjC */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */; };
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */; };
841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */; };
@ -437,6 +427,10 @@
84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; };
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; };
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; };
847C4C0E2CDF2301008BF5FE /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 847C4C0D2CDF2301008BF5FE /* RSTree */; };
847C4C0F2CDF2301008BF5FE /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 847C4C0D2CDF2301008BF5FE /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
847C4C112CDF2310008BF5FE /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 847C4C102CDF2310008BF5FE /* RSTree */; };
847C4C122CDF2310008BF5FE /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 847C4C102CDF2310008BF5FE /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; };
847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; };
848362FF2262A30E00DA1D35 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; };
@ -646,7 +640,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
4679674725E599C100844E8D /* Articles in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -657,9 +650,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
8413877C2CD897CF00E8490F /* RSCore in Embed Frameworks */,
8413877F2CD897CF00E8490F /* RSCoreObjC in Embed Frameworks */,
51BC2F4924D3439E00E90810 /* RSTree in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -670,8 +660,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
841387882CD89E5200E8490F /* RSCore in Embed Frameworks */,
51BC2F4E24D343AB00E90810 /* RSTree in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -682,8 +670,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
841387852CD897EF00E8490F /* RSCoreObjC in Embed Frameworks */,
841387822CD897EF00E8490F /* RSCore in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -707,15 +693,16 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
8417FA442CDF2E56005F989B /* RSDatabase in Embed Frameworks */,
841387922CDC791B00E8490F /* RSWeb in Embed Frameworks */,
841387762CD897C500E8490F /* RSCore in Embed Frameworks */,
847C4C122CDF2310008BF5FE /* RSTree in Embed Frameworks */,
513F32782593EE6F0003048F /* Secrets in Embed Frameworks */,
5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */,
8417FA472CDF2E7E005F989B /* RSDatabaseObjC in Embed Frameworks */,
513F327B2593EE6F0003048F /* SyncDatabase in Embed Frameworks */,
513F32722593EE6F0003048F /* Articles in Embed Frameworks */,
513F32812593EF180003048F /* Account in Embed Frameworks */,
5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */,
5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */,
841387792CD897C500E8490F /* RSCoreObjC in Embed Frameworks */,
513F32752593EE6F0003048F /* ArticlesDatabase in Embed Frameworks */,
);
@ -754,17 +741,18 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
8417FA412CDF2E4B005F989B /* RSDatabase in Embed Frameworks */,
8413878F2CDC790C00E8490F /* RSWeb in Embed Frameworks */,
8413876E2CD8970B00E8490F /* RSCore in Embed Frameworks */,
847C4C0F2CDF2301008BF5FE /* RSTree in Embed Frameworks */,
513277442590FBB60064F1E7 /* Account in Embed Frameworks */,
5132775F2590FC640064F1E7 /* Articles in Embed Frameworks */,
51A737C024DB197F0015FA66 /* RSDatabase in Embed Frameworks */,
8417FA4A2CDF2E8A005F989B /* RSDatabaseObjC in Embed Frameworks */,
513277662590FC780064F1E7 /* Secrets in Embed Frameworks */,
513277652590FC640064F1E7 /* SyncDatabase in Embed Frameworks */,
513277622590FC640064F1E7 /* ArticlesDatabase in Embed Frameworks */,
51A737C924DB19CC0015FA66 /* RSParser in Embed Frameworks */,
841387712CD8970B00E8490F /* RSCoreObjC in Embed Frameworks */,
514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -1054,6 +1042,7 @@
841387892CD8A38A00E8490F /* UniformTypeIdentifiers+Extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UniformTypeIdentifiers+Extras.swift"; sourceTree = "<group>"; };
8413878C2CDC78EE00E8490F /* RSWeb */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RSWeb; sourceTree = "<group>"; };
84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkCommandValidationStatus.swift; sourceTree = "<group>"; };
8417FA3E2CDF2E31005F989B /* RSDatabase */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RSDatabase; sourceTree = "<group>"; };
841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NothingInspectorViewController.swift; sourceTree = "<group>"; };
841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderInspectorViewController.swift; sourceTree = "<group>"; };
841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltinSmartFeedInspectorViewController.swift; sourceTree = "<group>"; };
@ -1084,6 +1073,7 @@
8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = "<group>"; };
847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = "<group>"; };
847C4C0C2CDF22DD008BF5FE /* RSTree */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RSTree; sourceTree = "<group>"; };
847CD6C9232F4CBF00FAC46D /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = "<group>"; };
848362FE2262A30E00DA1D35 /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = template.html; sourceTree = "<group>"; };
@ -1261,7 +1251,6 @@
files = (
27B86EEB25A53AAB00264340 /* Account in Frameworks */,
841387872CD89E5200E8490F /* RSCore in Frameworks */,
51BC2F4D24D343AB00E90810 /* RSTree in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1272,7 +1261,6 @@
51BC2F3824D3439A00E90810 /* Account in Frameworks */,
8413877B2CD897CF00E8490F /* RSCore in Frameworks */,
8413877E2CD897CF00E8490F /* RSCoreObjC in Frameworks */,
51BC2F4824D3439E00E90810 /* RSTree in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1297,9 +1285,11 @@
841387752CD897C500E8490F /* RSCore in Frameworks */,
179D280B26F6F93D003B2E0A /* Zip in Frameworks */,
516B695F24D2F33B00B5702F /* Account in Frameworks */,
8417FA462CDF2E7E005F989B /* RSDatabaseObjC in Frameworks */,
847C4C112CDF2310008BF5FE /* RSTree in Frameworks */,
8417FA432CDF2E56005F989B /* RSDatabase in Frameworks */,
841387782CD897C500E8490F /* RSCoreObjC in Frameworks */,
5138E95224D3418100AFF0FE /* RSParser in Frameworks */,
5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */,
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */,
513F32712593EE6F0003048F /* Articles in Frameworks */,
513F32772593EE6F0003048F /* Secrets in Frameworks */,
@ -1307,7 +1297,6 @@
841387912CDC791B00E8490F /* RSWeb in Frameworks */,
513F32742593EE6F0003048F /* ArticlesDatabase in Frameworks */,
513F327A2593EE6F0003048F /* SyncDatabase in Frameworks */,
5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1317,20 +1306,21 @@
files = (
513277642590FC640064F1E7 /* SyncDatabase in Frameworks */,
17192ADA2567B3D500AAEACA /* RSSparkle in Frameworks */,
514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */,
5132775E2590FC640064F1E7 /* Articles in Frameworks */,
513277612590FC640064F1E7 /* ArticlesDatabase in Frameworks */,
8417FA402CDF2E4B005F989B /* RSDatabase in Frameworks */,
51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */,
51A737C824DB19CC0015FA66 /* RSParser in Frameworks */,
8417FA492CDF2E8A005F989B /* RSDatabaseObjC in Frameworks */,
841387732CD8970B00E8490F /* RSCoreResources in Frameworks */,
8413878E2CDC790C00E8490F /* RSWeb in Frameworks */,
179C39EA26F76B0500D4E741 /* Zip in Frameworks */,
51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */,
841387702CD8970B00E8490F /* RSCoreObjC in Frameworks */,
847C4C0E2CDF2301008BF5FE /* RSTree in Frameworks */,
8413876D2CD8970B00E8490F /* RSCore in Frameworks */,
514C16CE24D2E63F009A3AFA /* Account in Frameworks */,
519CA8E525841DB700EB079A /* CrashReporter in Frameworks */,
51A737BF24DB197F0015FA66 /* RSDatabase in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2048,6 +2038,8 @@
51CD32C324D2CD57009ABAEF /* ArticlesDatabase */,
51CD32C724D2E06C009ABAEF /* Secrets */,
51CD32A824D2CB25009ABAEF /* SyncDatabase */,
8417FA3E2CDF2E31005F989B /* RSDatabase */,
847C4C0C2CDF22DD008BF5FE /* RSTree */,
8413878C2CDC78EE00E8490F /* RSWeb */,
8413876B2CD896E000E8490F /* RSCore */,
);
@ -2476,7 +2468,6 @@
name = "NetNewsWire iOS Intents Extension";
packageProductDependencies = (
51BC2F4A24D343A500E90810 /* Account */,
51BC2F4C24D343AB00E90810 /* RSTree */,
841387862CD89E5200E8490F /* RSCore */,
);
productName = "NetNewsWire iOS Intents Extension";
@ -2499,7 +2490,6 @@
name = "NetNewsWire iOS Share Extension";
packageProductDependencies = (
51BC2F3724D3439A00E90810 /* Account */,
51BC2F4724D3439E00E90810 /* RSTree */,
8413877A2CD897CF00E8490F /* RSCore */,
8413877D2CD897CF00E8490F /* RSCoreObjC */,
);
@ -2564,8 +2554,6 @@
name = "NetNewsWire-iOS";
packageProductDependencies = (
516B695E24D2F33B00B5702F /* Account */,
5138E93924D33E5600AFF0FE /* RSTree */,
5138E94B24D3417A00AFF0FE /* RSDatabase */,
5138E95124D3418100AFF0FE /* RSParser */,
513F32702593EE6F0003048F /* Articles */,
513F32732593EE6F0003048F /* ArticlesDatabase */,
@ -2575,6 +2563,9 @@
841387742CD897C500E8490F /* RSCore */,
841387772CD897C500E8490F /* RSCoreObjC */,
841387902CDC791B00E8490F /* RSWeb */,
847C4C102CDF2310008BF5FE /* RSTree */,
8417FA422CDF2E56005F989B /* RSDatabase */,
8417FA452CDF2E7E005F989B /* RSDatabaseObjC */,
);
productName = "NetNewsWire-iOS";
productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */;
@ -2604,9 +2595,7 @@
name = NetNewsWire;
packageProductDependencies = (
514C16CD24D2E63F009A3AFA /* Account */,
514C16DD24D2EF15009A3AFA /* RSTree */,
51C4CFF524D37DD500AF9874 /* Secrets */,
51A737BE24DB197F0015FA66 /* RSDatabase */,
51A737C724DB19CC0015FA66 /* RSParser */,
17192AD92567B3D500AAEACA /* RSSparkle */,
519CA8E425841DB700EB079A /* CrashReporter */,
@ -2618,6 +2607,9 @@
8413876F2CD8970B00E8490F /* RSCoreObjC */,
841387722CD8970B00E8490F /* RSCoreResources */,
8413878D2CDC790C00E8490F /* RSWeb */,
847C4C0D2CDF2301008BF5FE /* RSTree */,
8417FA3F2CDF2E4B005F989B /* RSDatabase */,
8417FA482CDF2E8A005F989B /* RSDatabaseObjC */,
);
productName = NetNewsWire;
productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */;
@ -2721,8 +2713,6 @@
);
mainGroup = 849C64571ED37A5D003D8FC0;
packageReferences = (
510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */,
51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */,
51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */,
17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */,
519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */,
@ -3900,14 +3890,6 @@
revision = 059e7346082d02de16220cd79df7db18ddeba8c3;
};
};
510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSTree.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/plcrashreporter.git";
@ -3916,14 +3898,6 @@
minimumVersion = 1.8.1;
};
};
51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSDatabase.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSParser.git";
@ -3970,16 +3944,6 @@
isa = XCSwiftPackageProductDependency;
productName = SyncDatabase;
};
5138E93924D33E5600AFF0FE /* RSTree */ = {
isa = XCSwiftPackageProductDependency;
package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
productName = RSTree;
};
5138E94B24D3417A00AFF0FE /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
5138E95124D3418100AFF0FE /* RSParser */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
@ -4005,11 +3969,6 @@
isa = XCSwiftPackageProductDependency;
productName = Account;
};
514C16DD24D2EF15009A3AFA /* RSTree */ = {
isa = XCSwiftPackageProductDependency;
package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
productName = RSTree;
};
516B695E24D2F33B00B5702F /* Account */ = {
isa = XCSwiftPackageProductDependency;
productName = Account;
@ -4019,11 +3978,6 @@
package = 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
51A737BE24DB197F0015FA66 /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
51A737C724DB19CC0015FA66 /* RSParser */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
@ -4033,20 +3987,10 @@
isa = XCSwiftPackageProductDependency;
productName = Account;
};
51BC2F4724D3439E00E90810 /* RSTree */ = {
isa = XCSwiftPackageProductDependency;
package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
productName = RSTree;
};
51BC2F4A24D343A500E90810 /* Account */ = {
isa = XCSwiftPackageProductDependency;
productName = Account;
};
51BC2F4C24D343AB00E90810 /* RSTree */ = {
isa = XCSwiftPackageProductDependency;
package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */;
productName = RSTree;
};
51C4CFF524D37DD500AF9874 /* Secrets */ = {
isa = XCSwiftPackageProductDependency;
productName = Secrets;
@ -4099,10 +4043,34 @@
isa = XCSwiftPackageProductDependency;
productName = RSWeb;
};
8417FA3F2CDF2E4B005F989B /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
productName = RSDatabase;
};
8417FA422CDF2E56005F989B /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
productName = RSDatabase;
};
8417FA452CDF2E7E005F989B /* RSDatabaseObjC */ = {
isa = XCSwiftPackageProductDependency;
productName = RSDatabaseObjC;
};
8417FA482CDF2E8A005F989B /* RSDatabaseObjC */ = {
isa = XCSwiftPackageProductDependency;
productName = RSDatabaseObjC;
};
845288232CDC90FA00C63564 /* RSWeb */ = {
isa = XCSwiftPackageProductDependency;
productName = RSWeb;
};
847C4C0D2CDF2301008BF5FE /* RSTree */ = {
isa = XCSwiftPackageProductDependency;
productName = RSTree;
};
847C4C102CDF2310008BF5FE /* RSTree */ = {
isa = XCSwiftPackageProductDependency;
productName = RSTree;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 849C64581ED37A5D003D8FC0 /* Project object */;

View File

@ -10,15 +10,6 @@
"version": "1.10.1"
}
},
{
"package": "RSDatabase",
"repositoryURL": "https://github.com/Ranchero-Software/RSDatabase.git",
"state": {
"branch": null,
"revision": "a6c5f1622320f745cc9a0a910d1bed1e2eaf15e3",
"version": "1.0.0"
}
},
{
"package": "RSParser",
"repositoryURL": "https://github.com/Ranchero-Software/RSParser.git",
@ -28,15 +19,6 @@
"version": "2.0.3"
}
},
{
"package": "RSTree",
"repositoryURL": "https://github.com/Ranchero-Software/RSTree.git",
"state": {
"branch": null,
"revision": "9d051f42cfc4faa991fd79cdb32e4cc8c545e334",
"version": "1.0.0"
}
},
{
"package": "RSSparkle",
"repositoryURL": "https://github.com/Ranchero-Software/Sparkle-Binary.git",

View File

@ -3,35 +3,34 @@
import PackageDescription
let package = Package(
name: "RSCore",
name: "RSCore",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(name: "RSCore", type: .dynamic, targets: ["RSCore"]),
products: [
.library(name: "RSCore", type: .dynamic, targets: ["RSCore"]),
.library(name: "RSCoreObjC", type: .dynamic, targets: ["RSCoreObjC"]),
.library(name: "RSCoreResources", type: .static, targets: ["RSCoreResources"])
],
],
targets: [
.target(
name: "RSCore",
dependencies: ["RSCoreObjC"],
swiftSettings: [
.unsafeFlags(["-warnings-as-errors"])
]),
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
),
.target(
name: "RSCoreObjC",
dependencies: [],
cSettings: [
.headerSearchPath("include")
]
]
),
.target(
name: "RSCoreResources",
resources: [
.process("Resources/WebViewWindow.xib"),
.process("Resources/IndeterminateProgressWindow.xib")
]),
.testTarget(
name: "RSCoreTests",
dependencies: ["RSCore"]),
]
name: "RSCoreResources",
resources: [
.process("Resources/WebViewWindow.xib"),
.process("Resources/IndeterminateProgressWindow.xib")
]),
.testTarget(
name: "RSCoreTests",
dependencies: ["RSCore"]),
]
)

18
RSDatabase/.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: CI
on: [push]
jobs:
build:
runs-on: macOS-latest
steps:
- name: Checkout Project
uses: actions/checkout@v1
- name: Switch to Xcode 12
run: sudo xcode-select -s /Applications/Xcode_12.app
- name: Run Build
run: swift build

61
RSDatabase/.gitignore vendored Normal file
View File

@ -0,0 +1,61 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
.build/
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xcuserstate
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
fastlane/report.xml
fastlane/screenshots
#Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/

21
RSDatabase/LICENSE Executable file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 brentsimmons
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

35
RSDatabase/Package.swift Normal file
View File

@ -0,0 +1,35 @@
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "RSDatabase",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "RSDatabase",
type: .dynamic,
targets: ["RSDatabase"]),
.library(
name: "RSDatabaseObjC",
type: .dynamic,
targets: ["RSDatabaseObjC"]),
],
dependencies: [
],
targets: [
.target(
name: "RSDatabase",
dependencies: ["RSDatabaseObjC"],
exclude: ["ODB/README.markdown"],
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
),
.target(
name: "RSDatabaseObjC",
dependencies: []
),
.testTarget(
name: "RSDatabaseTests",
dependencies: ["RSDatabase"]),
]
)

12
RSDatabase/README.md Executable file
View File

@ -0,0 +1,12 @@
# RSDatabase
This is utility code for using SQLite via FMDB. Its not a persistence framework — its lower-level.
It builds as a couple frameworks — one for Mac, one for iOS.
It has no additional dependencies, but thats because FMDB is actually included — you might want to instead make sure you have the [latest FMDB](https://github.com/ccgus/fmdb), which isnt necessarily included here.
#### What to look at
The main thing is `RSDatabaseQueue`, which allows you to talk to SQLite-via-FMDB using a serial queue.
The second thing is `FMDatabase+RSExtras`, which provides methods for a bunch of common queries and updates, so you dont have to write as much SQL.

View File

@ -0,0 +1,53 @@
//
// Database.swift
// RSDatabase
//
// Created by Brent Simmons on 12/15/19.
// Copyright © 2019 Brent Simmons. All rights reserved.
//
import Foundation
import RSDatabaseObjC
public enum DatabaseError: Error {
case isSuspended // On iOS, to support background refreshing, a database may be suspended.
}
/// Result type that provides an FMDatabase or a DatabaseError.
public typealias DatabaseResult = Result<FMDatabase, DatabaseError>
/// Block that executes database code or handles DatabaseQueueError.
public typealias DatabaseBlock = (DatabaseResult) -> Void
/// Completion block that provides an optional DatabaseError.
public typealias DatabaseCompletionBlock = (DatabaseError?) -> Void
/// Result type for fetching an Int or getting a DatabaseError.
public typealias DatabaseIntResult = Result<Int, DatabaseError>
/// Completion block for DatabaseIntResult.
public typealias DatabaseIntCompletionBlock = (DatabaseIntResult) -> Void
// MARK: - Extensions
public extension DatabaseResult {
/// Convenience for getting the database from a DatabaseResult.
var database: FMDatabase? {
switch self {
case .success(let database):
return database
case .failure:
return nil
}
}
/// Convenience for getting the error from a DatabaseResult.
var error: DatabaseError? {
switch self {
case .success:
return nil
case .failure(let error):
return error
}
}
}

View File

@ -0,0 +1,61 @@
//
// DatabaseObject.swift
// RSDatabase
//
// Created by Brent Simmons on 8/7/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public typealias DatabaseDictionary = [String: Any]
public protocol DatabaseObject {
var databaseID: String { get }
func databaseDictionary() -> DatabaseDictionary?
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]?
}
public extension DatabaseObject {
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
return nil
}
}
extension Array where Element == DatabaseObject {
func dictionary() -> [String: DatabaseObject] {
var d = [String: DatabaseObject]()
for object in self {
d[object.databaseID] = object
}
return d
}
func databaseIDs() -> Set<String> {
return Set(self.map { $0.databaseID })
}
func includesObjectWithDatabaseID(_ databaseID: String) -> Bool {
for object in self {
if object.databaseID == databaseID {
return true
}
}
return false
}
func databaseDictionaries() -> [DatabaseDictionary]? {
let dictionaries = self.compactMap{ $0.databaseDictionary() }
return dictionaries.isEmpty ? nil : dictionaries
}
}

View File

@ -0,0 +1,33 @@
//
// DatabaseObjectCache.swift
// RSDatabase
//
// Created by Brent Simmons on 9/12/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public final class DatabaseObjectCache {
private var d = [String: DatabaseObject]()
public init() {
//
}
public func add(_ databaseObjects: [DatabaseObject]) {
for databaseObject in databaseObjects {
self[databaseObject.databaseID] = databaseObject
}
}
public subscript(_ databaseID: String) -> DatabaseObject? {
get {
return d[databaseID]
}
set {
d[databaseID] = newValue
}
}
}

View File

@ -0,0 +1,259 @@
//
// DatabaseQueue.swift
// RSDatabase
//
// Created by Brent Simmons on 11/13/19.
// Copyright © 2019 Brent Simmons. All rights reserved.
//
import Foundation
import SQLite3
import RSDatabaseObjC
/// Manage a serial queue and a SQLite database.
/// It replaces RSDatabaseQueue, which is deprecated.
/// Main-thread only.
/// Important note: on iOS, the queue can be suspended
/// in order to support background refreshing.
public final class DatabaseQueue {
/// Check to see if the queue is suspended. Read-only.
/// Calling suspend() and resume() will change the value of this property.
/// This will return true only on iOS  on macOS its always false.
public var isSuspended: Bool {
#if os(iOS)
precondition(Thread.isMainThread)
return _isSuspended
#else
return false
#endif
}
private var _isSuspended = true
private var isCallingDatabase = false
private let database: FMDatabase
private let databasePath: String
private let serialDispatchQueue: DispatchQueue
private let targetDispatchQueue: DispatchQueue
#if os(iOS)
private let databaseLock = NSLock()
#endif
/// When init returns, the database will not be suspended: it will be ready for database calls.
public init(databasePath: String) {
precondition(Thread.isMainThread)
self.serialDispatchQueue = DispatchQueue(label: "DatabaseQueue (Serial) - \(databasePath)", attributes: .initiallyInactive)
self.targetDispatchQueue = DispatchQueue(label: "DatabaseQueue (Target) - \(databasePath)")
self.serialDispatchQueue.setTarget(queue: self.targetDispatchQueue)
self.serialDispatchQueue.activate()
self.databasePath = databasePath
self.database = FMDatabase(path: databasePath)!
openDatabase()
_isSuspended = false
}
// MARK: - Suspend and Resume
/// Close the SQLite database and dont allow database calls until resumed.
/// This is for iOS, where we need to close the SQLite database in some conditions.
///
/// After calling suspend, if you call into the database before calling resume,
/// your code will not run, and runInDatabaseSync and runInTransactionSync will
/// both throw DatabaseQueueError.isSuspended.
///
/// On Mac, suspend() and resume() are no-ops, since there isnt a need for them.
public func suspend() {
#if os(iOS)
precondition(Thread.isMainThread)
guard !_isSuspended else {
return
}
_isSuspended = true
serialDispatchQueue.suspend()
targetDispatchQueue.async {
self.lockDatabase()
self.database.close()
self.unlockDatabase()
DispatchQueue.main.async {
self.serialDispatchQueue.resume()
}
}
#endif
}
/// Open the SQLite database. Allow database calls again.
/// This is also for iOS only.
public func resume() {
#if os(iOS)
precondition(Thread.isMainThread)
guard _isSuspended else {
return
}
serialDispatchQueue.suspend()
targetDispatchQueue.sync {
if _isSuspended {
lockDatabase()
openDatabase()
unlockDatabase()
_isSuspended = false
}
}
serialDispatchQueue.resume()
#endif
}
// MARK: - Make Database Calls
/// Run a DatabaseBlock synchronously. This call will block the main thread
/// potentially for a while, depending on how long it takes to execute
/// the DatabaseBlock *and* depending on how many other calls have been
/// scheduled on the queue. Use sparingly  prefer async versions.
public func runInDatabaseSync(_ databaseBlock: DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.sync {
self._runInDatabase(self.database, databaseBlock, false)
}
}
/// Run a DatabaseBlock asynchronously.
public func runInDatabase(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.async {
self._runInDatabase(self.database, databaseBlock, false)
}
}
/// Run a DatabaseBlock wrapped in a transaction synchronously.
/// Transactions help performance significantly when updating the database.
/// Nevertheless, its best to avoid this because it will block the main thread
/// prefer the async `runInTransaction` instead.
public func runInTransactionSync(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.sync {
self._runInDatabase(self.database, databaseBlock, true)
}
}
/// Run a DatabaseBlock wrapped in a transaction asynchronously.
/// Transactions help performance significantly when updating the database.
public func runInTransaction(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.async {
self._runInDatabase(self.database, databaseBlock, true)
}
}
/// Run all the lines that start with "create".
/// Use this to create tables, indexes, etc.
public func runCreateStatements(_ statements: String) throws {
precondition(Thread.isMainThread)
var error: DatabaseError? = nil
runInDatabaseSync { result in
switch result {
case .success(let database):
statements.enumerateLines { (line, stop) in
if line.lowercased().hasPrefix("create") {
database.executeStatements(line)
}
stop = false
}
case .failure(let databaseError):
error = databaseError
}
}
if let error = error {
throw(error)
}
}
/// Compact the database. This should be done from time to time
/// weekly-ish? to keep up the performance level of a database.
/// Generally a thing to do at startup, if its been a while
/// since the last vacuum() call. You almost certainly want to call
/// vacuumIfNeeded instead.
public func vacuum() {
precondition(Thread.isMainThread)
runInDatabase { result in
result.database?.executeStatements("vacuum;")
}
}
/// Vacuum the database if its been more than `daysBetweenVacuums` since the last vacuum.
/// Normally you would call this right after initing a DatabaseQueue.
///
/// - Returns: true if database will be vacuumed.
@discardableResult
public func vacuumIfNeeded(daysBetweenVacuums: Int) -> Bool {
precondition(Thread.isMainThread)
let defaultsKey = "DatabaseQueue-LastVacuumDate-\(databasePath)"
let minimumVacuumInterval = TimeInterval(daysBetweenVacuums * (60 * 60 * 24)) // Doesnt have to be precise
let now = Date()
let cutoffDate = now - minimumVacuumInterval
if let lastVacuumDate = UserDefaults.standard.object(forKey: defaultsKey) as? Date {
if lastVacuumDate < cutoffDate {
vacuum()
UserDefaults.standard.set(now, forKey: defaultsKey)
return true
}
return false
}
// Never vacuumed  almost certainly a new database.
// Just set the LastVacuumDate pref to now and skip vacuuming.
UserDefaults.standard.set(now, forKey: defaultsKey)
return false
}
}
private extension DatabaseQueue {
func lockDatabase() {
#if os(iOS)
databaseLock.lock()
#endif
}
func unlockDatabase() {
#if os(iOS)
databaseLock.unlock()
#endif
}
func _runInDatabase(_ database: FMDatabase, _ databaseBlock: DatabaseBlock, _ useTransaction: Bool) {
lockDatabase()
defer {
unlockDatabase()
}
precondition(!isCallingDatabase)
isCallingDatabase = true
autoreleasepool {
if _isSuspended {
databaseBlock(.failure(.isSuspended))
}
else {
if useTransaction {
database.beginTransaction()
}
databaseBlock(.success(database))
if useTransaction {
database.commit()
}
}
}
isCallingDatabase = false
}
func openDatabase() {
database.open()
database.executeStatements("PRAGMA synchronous = 1;")
database.setShouldCacheStatements(true)
}
}

View File

@ -0,0 +1,139 @@
//
// DatabaseTable.swift
// RSDatabase
//
// Created by Brent Simmons on 7/16/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
public protocol DatabaseTable {
var name: String { get }
}
public extension DatabaseTable {
// MARK: Fetching
func selectRowsWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? {
return database.rs_selectRowsWhereKey(key, equalsValue: value, tableName: name)
}
func selectSingleRowWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? {
return database.rs_selectSingleRowWhereKey(key, equalsValue: value, tableName: name)
}
func selectRowsWhere(key: String, inValues values: [Any], in database: FMDatabase) -> FMResultSet? {
if values.isEmpty {
return nil
}
return database.rs_selectRowsWhereKey(key, inValues: values, tableName: name)
}
// MARK: Deleting
func deleteRowsWhere(key: String, equalsAnyValue values: [Any], in database: FMDatabase) {
if values.isEmpty {
return
}
database.rs_deleteRowsWhereKey(key, inValues: values, tableName: name)
}
// MARK: Updating
func updateRowsWithValue(_ value: Any, valueKey: String, whereKey: String, matches: [Any], database: FMDatabase) {
let _ = database.rs_updateRows(withValue: value, valueKey: valueKey, whereKey: whereKey, inValues: matches, tableName: self.name)
}
func updateRowsWithDictionary(_ dictionary: DatabaseDictionary, whereKey: String, matches: Any, database: FMDatabase) {
let _ = database.rs_updateRows(with: dictionary, whereKey: whereKey, equalsValue: matches, tableName: self.name)
}
// MARK: Saving
func insertRows(_ dictionaries: [DatabaseDictionary], insertType: RSDatabaseInsertType, in database: FMDatabase) {
dictionaries.forEach { (oneDictionary) in
let _ = database.rs_insertRow(with: oneDictionary, insertType: insertType, tableName: self.name)
}
}
func insertRow(_ rowDictionary: DatabaseDictionary, insertType: RSDatabaseInsertType, in database: FMDatabase) {
insertRows([rowDictionary], insertType: insertType, in: database)
}
// MARK: Counting
func numberWithCountResultSet(_ resultSet: FMResultSet) -> Int {
guard resultSet.next() else {
return 0
}
return Int(resultSet.int(forColumnIndex: 0))
}
func numberWithSQLAndParameters(_ sql: String, _ parameters: [Any], in database: FMDatabase) -> Int {
if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) {
return numberWithCountResultSet(resultSet)
}
return 0
}
// MARK: Mapping
func mapResultSet<T>(_ resultSet: FMResultSet, _ completion: (_ resultSet: FMResultSet) -> T?) -> [T] {
var objects = [T]()
while resultSet.next() {
if let obj = completion(resultSet) {
objects += [obj]
}
}
return objects
}
// MARK: Columns
func containsColumn(_ columnName: String, in database: FMDatabase) -> Bool {
if let resultSet = database.executeQuery("select * from \(name) limit 1;", withArgumentsIn: nil) {
if let columnMap = resultSet.columnNameToIndexMap {
if let _ = columnMap[columnName.lowercased()] {
return true
}
}
}
return false
}
}
public extension FMResultSet {
func compactMap<T>(_ completion: (_ row: FMResultSet) -> T?) -> [T] {
var objects = [T]()
while next() {
if let obj = completion(self) {
objects += [obj]
}
}
close()
return objects
}
func mapToSet<T>(_ completion: (_ row: FMResultSet) -> T?) -> Set<T> {
return Set(compactMap(completion))
}
}

View File

@ -0,0 +1,179 @@
//
// ODB.swift
// RSDatabase
//
// Created by Brent Simmons on 4/20/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
// This is not thread-safe. Neither are the other ODB* objects and structs.
// Its up to the caller to implement thread safety.
public final class ODB: Hashable {
public let filepath: String
public var isClosed: Bool {
return _closed
}
static let rootTableID = -1
public lazy var rootTable: ODBTable? = {
ODBTable(uniqueID: ODB.rootTableID, name: ODBPath.rootTableName, parentTable: nil, isRootTable: true, odb: self)
}()
private var _closed = false
private let queue: RSDatabaseQueue
private var odbTablesTable: ODBTablesTable? = ODBTablesTable()
private var odbValuesTable: ODBValuesTable? = ODBValuesTable()
public init(filepath: String) {
self.filepath = filepath
let queue = RSDatabaseQueue(filepath: filepath, excludeFromBackup: false)
queue.createTables(usingStatementsSync: ODB.tableCreationStatements)
self.queue = queue
}
/// Call when finished, to make sure no stray references can do undefined things.
/// Its not necessary to call this on app termination.
public func close() {
guard !_closed else {
return
}
_closed = true
queue.close()
odbValuesTable = nil
odbTablesTable = nil
rootTable?.close()
rootTable = nil
}
/// Get a reference to an ODBTable at a path, making sure it exists.
/// Returns nil if theres a value in the path preventing the table from being made.
public func ensureTable(_ path: ODBPath) -> ODBTable? {
return path.ensureTable(with: self)
}
/// Compact the database on disk.
public func vacuum() {
queue.vacuum()
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(filepath)
}
// MARK: - Equatable
public static func ==(lhs: ODB, rhs: ODB) -> Bool {
return lhs.filepath == rhs.filepath
}
}
extension ODB {
func delete(_ object: ODBObject) -> Bool {
guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else {
return false
}
if let valueObject = object as? ODBValueObject {
let uniqueID = valueObject.uniqueID
queue.updateSync { (database) in
odbValuesTable.deleteObject(uniqueID: uniqueID, database: database)
}
}
else if let tableObject = object as? ODBTable {
let uniqueID = tableObject.uniqueID
queue.updateSync { (database) in
odbTablesTable.deleteTable(uniqueID: uniqueID, database: database)
}
}
return true
}
func deleteChildren(of table: ODBTable) -> Bool {
guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else {
return false
}
let parentUniqueID = table.uniqueID
queue.updateSync { (database) in
odbTablesTable.deleteChildTables(parentUniqueID: parentUniqueID, database: database)
odbValuesTable.deleteChildObjects(parentUniqueID: parentUniqueID, database: database)
}
return true
}
func insertTable(name: String, parent: ODBTable) -> ODBTable? {
guard let odbTablesTable = odbTablesTable else {
return nil
}
var table: ODBTable? = nil
queue.fetchSync { (database) in
table = odbTablesTable.insertTable(name: name, parentTable: parent, odb: self, database: database)
}
return table!
}
func insertValueObject(name: String, value: ODBValue, parent: ODBTable) -> ODBValueObject? {
guard let odbValuesTable = odbValuesTable else {
return nil
}
var valueObject: ODBValueObject? = nil
queue.updateSync { (database) in
valueObject = odbValuesTable.insertValueObject(name: name, value: value, parentTable: parent, database: database)
}
return valueObject!
}
func fetchChildren(of table: ODBTable) -> ODBDictionary {
guard let odbValuesTable = odbValuesTable, let odbTablesTable = odbTablesTable else {
return ODBDictionary()
}
var children = ODBDictionary()
queue.fetchSync { (database) in
let tables = odbTablesTable.fetchSubtables(of: table, database: database, odb: self)
let valueObjects = odbValuesTable.fetchValueObjects(of: table, database: database)
// Keys are lower-cased, since we case-insensitive lookups.
for valueObject in valueObjects {
children[valueObject.name] = valueObject
}
for table in tables {
children[table.name] = table
}
}
return children
}
}
private extension ODB {
static let tableCreationStatements = """
CREATE TABLE if not EXISTS odb_tables (id INTEGER PRIMARY KEY AUTOINCREMENT, parent_id INTEGER NOT NULL, name TEXT NOT NULL);
CREATE TABLE if not EXISTS odb_values (id INTEGER PRIMARY KEY AUTOINCREMENT, odb_table_id INTEGER NOT NULL, name TEXT NOT NULL, primitive_type INTEGER NOT NULL, application_type TEXT, value BLOB);
CREATE INDEX if not EXISTS odb_tables_parent_id_index on odb_tables (parent_id);
CREATE INDEX if not EXISTS odb_values_odb_table_id_index on odb_values (odb_table_id);
CREATE TRIGGER if not EXISTS odb_tables_after_delete_trigger_delete_subtables after delete on odb_tables begin delete from odb_tables where parent_id = OLD.id; end;
CREATE TRIGGER if not EXISTS odb_tables_after_delete_trigger_delete_child_values after delete on odb_tables begin delete from odb_values where odb_table_id = OLD.id; end;
"""
}

View File

@ -0,0 +1,18 @@
//
// ODBObject.swift
// RSDatabase
//
// Created by Brent Simmons on 4/24/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public typealias ODBDictionary = [String: ODBObject]
// ODBTable and ODBValueObject conform to ODBObject.
public protocol ODBObject {
var name: String { get }
var parentTable: ODBTable? { get }
}

View File

@ -0,0 +1,196 @@
//
// ODBPath.swift
// RSDatabase
//
// Created by Brent Simmons on 4/21/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
/**
An ODBPath is an array like ["system", "verbs", "apps", "Xcode"].
The first element in the array may be "root". If so, its ignored: "root" is implied.
An empty array or ["root"] refers to the root table.
A path does not necessarily point to something that exists. Its like file paths or URLs.
*/
public struct ODBPath: Hashable {
/// The last element in the path. May not have same capitalization as canonical name in the database.
public let name: String
/// True if this path points to a root table.
public let isRoot: Bool
/// Root table name. Constant.
public static let rootTableName = "root"
/// Elements of the path minus any unneccessary initial "root" element.
public let elements: [String]
/// ODBPath that represents the root table.
public static let root = ODBPath.path([String]())
/// The optional path to the parent table. Nil only if path is to the root table.
public var parentTablePath: ODBPath? {
if isRoot {
return nil
}
return ODBPath.path(Array(elements.dropLast()))
}
private static var pathCache = [[String]: ODBPath]()
private static let pathCacheLock = NSLock()
private init(elements: [String]) {
let canonicalElements = ODBPath.dropLeadingRootElement(from: elements)
self.elements = canonicalElements
if canonicalElements.count < 1 {
self.name = ODBPath.rootTableName
self.isRoot = true
}
else {
self.name = canonicalElements.last!
self.isRoot = false
}
}
// MARK: - API
/// Create a path.
public static func path(_ elements: [String]) -> ODBPath {
pathCacheLock.lock()
defer {
pathCacheLock.unlock()
}
if let cachedPath = pathCache[elements] {
return cachedPath
}
let path = ODBPath(elements: elements)
pathCache[elements] = path
return path
}
/// Create a path by adding an element.
public func pathByAdding(_ element: String) -> ODBPath {
return ODBPath.path(elements + [element])
}
/// Create a path by adding an element.
public static func +(lhs: ODBPath, rhs: String) -> ODBPath {
return lhs.pathByAdding(rhs)
}
/// Fetch the database object at this path.
public func odbObject(with odb: ODB) -> ODBObject? {
return resolvedObject(odb)
}
/// Fetch the value at this path.
public func odbValue(with odb: ODB) -> ODBValue? {
return parentTable(with: odb)?.odbValue(name)
}
/// Set a value for this path. Will overwrite existing value or table.
public func setODBValue(_ value: ODBValue, odb: ODB) -> Bool {
return parentTable(with: odb)?.set(value, name: name) ?? false
}
/// Fetch the raw value at this path.
public func rawValue(with odb: ODB) -> Any? {
return parentTable(with: odb)?.rawValue(name)
}
/// Set the raw value for this path. Will overwrite existing value or table.
@discardableResult
public func setRawValue(_ rawValue: Any, odb: ODB) -> Bool {
return parentTable(with: odb)?.set(rawValue, name: name) ?? false
}
/// Delete value or table at this path.
public func delete(from odb: ODB) -> Bool {
return parentTable(with: odb)?.delete(name: name) ?? false
}
/// Fetch the table at this path.
public func table(with odb: ODB) -> ODBTable? {
return odbObject(with: odb) as? ODBTable
}
/// Fetch the parent table. Nil if this is the root table.
public func parentTable(with odb: ODB) -> ODBTable? {
return parentTablePath?.table(with: odb)
}
/// Creates a table  will delete existing table.
public func createTable(with odb: ODB) -> ODBTable? {
return parentTable(with: odb)?.addSubtable(name: name)
}
/// Return the table for the final item in the path.
/// Wont delete anything.
@discardableResult
public func ensureTable(with odb: ODB) -> ODBTable? {
if isRoot {
return odb.rootTable
}
if let existingObject = odbObject(with: odb) {
if let existingTable = existingObject as? ODBTable {
return existingTable
}
return nil // It must be a value: dont overwrite.
}
if let parentTable = parentTablePath!.ensureTable(with: odb) {
return parentTable.addSubtable(name: name)
}
return nil
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(elements)
}
// MARK: - Equatable
public static func ==(lhs: ODBPath, rhs: ODBPath) -> Bool {
return lhs.elements == rhs.elements
}
}
// MARK: - Private
private extension ODBPath {
func resolvedObject(_ odb: ODB) -> ODBObject? {
if isRoot {
return odb.rootTable
}
guard let table = parentTable(with: odb) else {
return nil
}
return table[name]
}
static func dropLeadingRootElement(from elements: [String]) -> [String] {
if elements.count < 1 {
return elements
}
let firstElement = elements.first!
if firstElement == ODBPath.rootTableName {
return Array(elements.dropFirst())
}
return elements
}
}

View File

@ -0,0 +1,42 @@
//
// ODBRawValueTable.swift
// RSDatabase
//
// Created by Brent Simmons on 9/13/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Use this when youre just getting/setting raw values from a table.
public final class ODBRawValueTable {
let table: ODBTable
init(table: ODBTable) {
self.table = table
}
public subscript(_ name: String) -> Any? {
get {
return table.rawValue(name)
}
set {
if let rawValue = newValue {
table.set(rawValue, name: name)
}
else {
table.delete(name: name)
}
}
}
public func string(for name: String) -> String? {
return self[name] as? String
}
public func setString(_ stringValue: String?, for name: String) {
self[name] = stringValue
}
}

View File

@ -0,0 +1,170 @@
//
// ODBTable.swift
// RSDatabase
//
// Created by Brent Simmons on 4/21/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public final class ODBTable: ODBObject, Hashable {
let uniqueID: Int
public let isRootTable: Bool
public let odb: ODB
public let parentTable: ODBTable?
public let name: String
public let path: ODBPath
private var _children: ODBDictionary?
public var children: ODBDictionary {
get {
if _children == nil {
_children = odb.fetchChildren(of: self)
}
return _children!
}
set {
_children = newValue
}
}
public lazy var rawValueTable = {
return ODBRawValueTable(table: self)
}()
init(uniqueID: Int, name: String, parentTable: ODBTable?, isRootTable: Bool, odb: ODB) {
self.uniqueID = uniqueID
self.name = name
self.parentTable = parentTable
self.isRootTable = isRootTable
self.path = isRootTable ? ODBPath.root : parentTable!.path + name
self.odb = odb
}
/// Get the ODBObject for the given name.
public subscript(_ name: String) -> ODBObject? {
return children[name]
}
/// Fetch the ODBValue for the given name.
public func odbValue(_ name: String) -> ODBValue? {
return (self[name] as? ODBValueObject)?.value
}
/// Set the ODBValue for the given name.
public func set(_ odbValue: ODBValue, name: String) -> Bool {
// Dont bother if key/value pair already exists.
// If child with same name exists, delete it.
let existingObject = self[name]
if let existingValue = existingObject as? ODBValueObject, existingValue.value == odbValue {
return true
}
guard let valueObject = odb.insertValueObject(name: name, value: odbValue, parent: self) else {
return false
}
if let existingObject = existingObject {
delete(existingObject)
}
addChild(name: name, object: valueObject)
return true
}
/// Fetch the raw value for the given name.
public func rawValue(_ name: String) -> Any? {
return (self[name] as? ODBValueObject)?.value.rawValue
}
/// Create a value object and set it for the given name.
@discardableResult
public func set(_ rawValue: Any, name: String) -> Bool {
guard let odbValue = ODBValue(rawValue: rawValue) else {
return false
}
return set(odbValue, name: name)
}
/// Delete all children  empty the table.
public func deleteChildren() -> Bool {
guard odb.deleteChildren(of: self) else {
return false
}
_children = ODBDictionary()
return true
}
/// Delete a child object.
@discardableResult
public func delete(_ object: ODBObject) -> Bool {
return odb.delete(object)
}
/// Delete a child with the given name.
@discardableResult
public func delete(name: String) -> Bool {
guard let child = self[name] else {
return false
}
return delete(child)
}
/// Fetch the subtable with the given name.
public func subtable(name: String) -> ODBTable? {
return self[name] as? ODBTable
}
/// Add a subtable with the given name. Overwrites previous child with that name.
public func addSubtable(name: String) -> ODBTable? {
let existingObject = self[name]
guard let subTable = odb.insertTable(name: name, parent: self) else {
return nil
}
if let existingObject = existingObject {
delete(existingObject)
}
addChild(name: name, object: subTable)
return subTable
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(uniqueID)
hasher.combine(odb)
}
// MARK: - Equatable
public static func ==(lhs: ODBTable, rhs: ODBTable) -> Bool {
return lhs.uniqueID == rhs.uniqueID && lhs.odb == rhs.odb
}
}
extension ODBTable {
func close() {
// Called from ODB when database is closing.
if let rawChildren = _children {
rawChildren.forEach { (key: String, value: ODBObject) in
if let table = value as? ODBTable {
table.close()
}
}
}
_children = nil
}
}
private extension ODBTable {
func addChild(name: String, object: ODBObject) {
children[name] = object
}
func ensureChildren() {
let _ = children
}
}

View File

@ -0,0 +1,56 @@
//
// ODBTablesTable.swift
// RSDatabase
//
// Created by Brent Simmons on 4/20/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
final class ODBTablesTable: DatabaseTable {
let name = "odb_tables"
private struct Key {
static let uniqueID = "id"
static let parentID = "parent_id"
static let name = "name"
}
func fetchSubtables(of table: ODBTable, database: FMDatabase, odb: ODB) -> Set<ODBTable> {
guard let rs: FMResultSet = database.executeQuery("select * from odb_tables where parent_id = ?", withArgumentsIn: [table.uniqueID]) else {
return Set<ODBTable>()
}
return rs.mapToSet{ createTable(with: $0, parentTable: table, odb: odb) }
}
func insertTable(name: String, parentTable: ODBTable, odb: ODB, database: FMDatabase) -> ODBTable {
let d: DatabaseDictionary = [Key.parentID: parentTable.uniqueID, Key.name: name]
insertRow(d, insertType: .normal, in: database)
let uniqueID = Int(database.lastInsertRowId())
return ODBTable(uniqueID: uniqueID, name: name, parentTable: parentTable, isRootTable: false, odb: odb)
}
func deleteTable(uniqueID: Int, database: FMDatabase) {
database.rs_deleteRowsWhereKey(Key.uniqueID, equalsValue: uniqueID, tableName: name)
}
func deleteChildTables(parentUniqueID: Int, database: FMDatabase) {
database.rs_deleteRowsWhereKey(Key.parentID, equalsValue: parentUniqueID, tableName: name)
}
}
private extension ODBTablesTable {
func createTable(with row: FMResultSet, parentTable: ODBTable, odb: ODB) -> ODBTable? {
guard let name = row.string(forColumn: Key.name) else {
return nil
}
let uniqueID = Int(row.longLongInt(forColumn: Key.uniqueID))
return ODBTable(uniqueID: uniqueID, name: name, parentTable: parentTable, isRootTable: false, odb: odb)
}
}

View File

@ -0,0 +1,164 @@
//
// ODBValue.swift
// RSDatabase
//
// Created by Brent Simmons on 4/24/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct ODBValue: Hashable {
// Values are arbitrary but must not change: theyre stored in the database.
public enum PrimitiveType: Int {
case boolean=8
case integer=16
case double=32
case date=64
case string=128
case data=256
}
public let rawValue: Any
public let primitiveType: PrimitiveType
public let applicationType: String? // Application-defined
public init(rawValue: Any, primitiveType: PrimitiveType, applicationType: String?) {
self.rawValue = rawValue
self.primitiveType = primitiveType
self.applicationType = applicationType
}
public init(rawValue: Any, primitiveType: PrimitiveType) {
self.init(rawValue: rawValue, primitiveType: primitiveType, applicationType: nil)
}
public init?(rawValue: Any) {
guard let primitiveType = ODBValue.primitiveTypeForRawValue(rawValue) else {
return nil
}
self.init(rawValue: rawValue, primitiveType: primitiveType)
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
if let booleanValue = rawValue as? Bool {
hasher.combine(booleanValue)
}
else if let integerValue = rawValue as? Int {
hasher.combine(integerValue)
}
else if let doubleValue = rawValue as? Double {
hasher.combine(doubleValue)
}
else if let stringValue = rawValue as? String {
hasher.combine(stringValue)
}
else if let dataValue = rawValue as? Data {
hasher.combine(dataValue)
}
else if let dateValue = rawValue as? Date {
hasher.combine(dateValue)
}
hasher.combine(primitiveType)
hasher.combine(applicationType)
}
// MARK: - Equatable
public static func ==(lhs: ODBValue, rhs: ODBValue) -> Bool {
if lhs.primitiveType != rhs.primitiveType || lhs.applicationType != rhs.applicationType {
return false
}
switch lhs.primitiveType {
case .boolean:
return compareBooleans(lhs.rawValue, rhs.rawValue)
case .integer:
return compareIntegers(lhs.rawValue, rhs.rawValue)
case .double:
return compareDoubles(lhs.rawValue, rhs.rawValue)
case .string:
return compareStrings(lhs.rawValue, rhs.rawValue)
case .data:
return compareData(lhs.rawValue, rhs.rawValue)
case .date:
return compareDates(lhs.rawValue, rhs.rawValue)
}
}
}
private extension ODBValue {
static func compareBooleans(_ left: Any, _ right: Any) -> Bool {
guard let left = left as? Bool, let right = right as? Bool else {
return false
}
return left == right
}
static func compareIntegers(_ left: Any, _ right: Any) -> Bool {
guard let left = left as? Int, let right = right as? Int else {
return false
}
return left == right
}
static func compareDoubles(_ left: Any, _ right: Any) -> Bool {
guard let left = left as? Double, let right = right as? Double else {
return false
}
return left == right
}
static func compareStrings(_ left: Any, _ right: Any) -> Bool {
guard let left = left as? String, let right = right as? String else {
return false
}
return left == right
}
static func compareData(_ left: Any, _ right: Any) -> Bool {
guard let left = left as? Data, let right = right as? Data else {
return false
}
return left == right
}
static func compareDates(_ left: Any, _ right: Any) -> Bool {
guard let left = left as? Date, let right = right as? Date else {
return false
}
return left == right
}
static func primitiveTypeForRawValue(_ rawValue: Any) -> ODBValue.PrimitiveType? {
switch rawValue {
case is Bool:
return .boolean
case is Int:
return .integer
case is Double:
return .double
case is Date:
return .date
case is String:
return .string
case is Data:
return .data
default:
return nil
}
}
}

View File

@ -0,0 +1,40 @@
//
// ODBValueObject.swift
// RSDatabase
//
// Created by Brent Simmons on 4/21/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct ODBValueObject: ODBObject, Hashable {
let uniqueID: Int
public let value: ODBValue
// ODBObject protocol properties
public let name: String
public let parentTable: ODBTable?
init(uniqueID: Int, parentTable: ODBTable, name: String, value: ODBValue) {
self.uniqueID = uniqueID
self.parentTable = parentTable
self.name = name
self.value = value
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(uniqueID)
hasher.combine(value)
}
// MARK: - Equatable
public static func ==(lhs: ODBValueObject, rhs: ODBValueObject) -> Bool {
return lhs.uniqueID == rhs.uniqueID && lhs.value == rhs.value
}
}

View File

@ -0,0 +1,97 @@
//
// ODBValuesTable.swift
// RSDatabase
//
// Created by Brent Simmons on 4/20/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
final class ODBValuesTable: DatabaseTable {
let name = "odb_values"
private struct Key {
static let uniqueID = "id"
static let parentID = "odb_table_id"
static let name = "name"
static let primitiveType = "primitive_type"
static let applicationType = "application_type"
static let value = "value"
}
func fetchValueObjects(of table: ODBTable, database: FMDatabase) -> Set<ODBValueObject> {
guard let rs = database.rs_selectRowsWhereKey(Key.parentID, equalsValue: table.uniqueID, tableName: name) else {
return Set<ODBValueObject>()
}
return rs.mapToSet{ valueObject(with: $0, parentTable: table) }
}
func deleteObject(uniqueID: Int, database: FMDatabase) {
database.rs_deleteRowsWhereKey(Key.uniqueID, equalsValue: uniqueID, tableName: name)
}
func deleteChildObjects(parentUniqueID: Int, database: FMDatabase) {
database.rs_deleteRowsWhereKey(Key.parentID, equalsValue: parentUniqueID, tableName: name)
}
func insertValueObject(name: String, value: ODBValue, parentTable: ODBTable, database: FMDatabase) -> ODBValueObject {
var d: DatabaseDictionary = [Key.parentID: parentTable.uniqueID, Key.name: name, Key.primitiveType: value.primitiveType.rawValue, Key.value: value.rawValue]
if let applicationType = value.applicationType {
d[Key.applicationType] = applicationType
}
insertRow(d, insertType: .normal, in: database)
let uniqueID = Int(database.lastInsertRowId())
return ODBValueObject(uniqueID: uniqueID, parentTable: parentTable, name: name, value: value)
}
}
private extension ODBValuesTable {
func valueObject(with row: FMResultSet, parentTable: ODBTable) -> ODBValueObject? {
guard let value = value(with: row) else {
return nil
}
guard let name = row.string(forColumn: Key.name) else {
return nil
}
let uniqueID = Int(row.longLongInt(forColumn: Key.uniqueID))
return ODBValueObject(uniqueID: uniqueID, parentTable: parentTable, name: name, value: value)
}
func value(with row: FMResultSet) -> ODBValue? {
guard let primitiveType = ODBValue.PrimitiveType(rawValue: Int(row.longLongInt(forColumn: Key.primitiveType))) else {
return nil
}
var value: Any? = nil
switch primitiveType {
case .boolean:
value = row.bool(forColumn: Key.value)
case .integer:
value = Int(row.longLongInt(forColumn: Key.value))
case .double:
value = row.double(forColumn: Key.value)
case .string:
value = row.string(forColumn: Key.value)
case .data:
value = row.data(forColumn: Key.value)
case .date:
value = row.date(forColumn: Key.value)
}
guard let fetchedValue = value else {
return nil
}
let applicationType = row.string(forColumn: Key.applicationType)
return ODBValue(rawValue: fetchedValue, primitiveType: primitiveType, applicationType: applicationType)
}
}

View File

@ -0,0 +1,149 @@
# ODB
**NOTE**: This all has been excluded from building. Its a work in progress, not ready for use.
ODB stands for Object Database.
“Object” doesnt mean object in the object-oriented programming sense — it just means *thing*.
Think of the ODB as a nested Dictionary thats *persistent*. Its schema-less. Tables (which are like dictionaries) can contain other tables. Its all key-value pairs.
The inspiration for this comes from [UserLand Frontier](http://frontier.userland.com/), which featured an ODB which made persistence for scripts easy.
You could write a script like `user.personalInfo.name = "Bull Mancuso"` — and, inside the `personalInfo` table, which is inside the `user` table, it would create or set a key/value pair: `name` would be the key, and `Bull Mancuso` would be the value.
Looking up the value later was as simple as referring to `user.personalInfo.name`.
This ODB implementation does *not* provide that scripting language. It also does not provide a user interface for the database (Frontier did). It provides just the lowest level: the actual storage and a Swift API for getting, setting, and deleting tables and values.
Its built on top of SQLite. It may sound weird to build an ODB on top of a SQL database — but SQLite is amazingly robust and fast, and its the hammer I know best.
My hunch is that lots of apps could benefit from this kind of storage. It was the *only* kind I used for seven years in my early career, and we wrote lots of powerful software using Frontiers ODB. (Blogging, RSS, podcasting, web services over HTTP, OPML — all these were invented or popularized or fleshed-out using Frontier and its ODB. Not that I take personal credit: I was an employee of UserLand Software, and the vision was Dave Winers.)
## How to use it
### Create an ODB
`let odb = ODB(filepath: somePath)` creates a new ODB for that path. If theres an existing database on disk, it uses that one. Otherwise it creates a new one.
### Ensuring that a table exists
Lets say youre writing an RSS reader, and you want to make sure theres a table at `RSS.feeds.[feedID]`. Given feedID and odb:
let pathElements = ["RSS", "feeds", feedID]
let path = ODBPath(elements: pathElements, odb: odb)
ODB.perform {
let _ = path.ensureTable()
}
The `ensureTable` function returns an `ODBTable`. It makes sure that the entire path exists. The only way `ensureTable` would return nil is if something in the path exists and its a value instead of a table. `ensureTable` never deletes anything.
There is a similar `createTable` function that deletes any existing table at that path and then creates a new table. It does *not* ensure that the entire path exists, and it returns nil if the necessary ancestor tables dont exist.
Operations referencing `ODBTable` and `ODBValueObject` must be enclosed in an `ODB.perform` block. This is for thread safety. If you dont use an `ODB.perform` block, it will crash deliberately with a `preconditionFailure`.
You should *not* hold a reference to an `ODBTable`, `ODBValueObject`, or `ODBObject` outside of the `perform` block. You *can* hold a reference to an `ODBPath` and to an `ODBValue`.
An `ODBObject` is either an `ODBTable` or `ODBValueObject`: its a protocol.
### Setting a value
Lets say the user of your RSS reader can edit the name of a feed, and you want to store the edited name in the database. The key for the value is `editedName`. Assume that youve already used `ensureTable` as above.
let path = ODBPath(elements: ["RSS", "feeds", feedID, "editedName"], odb: odb)
let value = ODBValue(value: name, primitiveType: .string, applicationType: nil)
ODB.perform {
path.setValue(value)
}
If `editedName` exists, it gets replaced. If it doesnt exist, then it gets created.
(Yes, this would be so much easier in a scripting language. Youd just write: `RSS.feeds.[feedID].editedName = name` — the above is the equivalent of that.)
See `ODBValue` for the different types of values that can be stored. Each value must be one of a few primitive types — string, date, data, etc. — but each value can optionally have its own `applicationType`. For instance, you might store OPML text as a string, but then give it an `applicationType` of `"OPML"`, so that your application knows what it is and can encode/decode properly. This lets you store values of any arbitrary complexity.
In general, its good practice to use that ability sparingly. When you can break things down into simple primitive types, thats best. Treating an entire table, with multiple stored values, as a unit is often the way to go. But not always.
### Getting a value
Lets say you want to get back the edited name of the feed. Youd create the path the same way as before. And then:
var nameValue: ODBValue? = nil
ODB.perform {
nameValue = path.value
}
let name = nameValue? as? String
The above is written to demonstrate that you can refer to `ODBValue` outside of a `perform` call. Its an immutable struct with no connection to the database. But in reality youd probably write the above code more like this:
var name: String?
ODB.perform {
name = path.value? as? String
}
Its totally a-okay to use Swifts built-in types this way instead of checking the ODBValues `primitiveType`. The primitive types map directly to `Bool`, `Int`, `Double`, `Date`, `String`, and `Data`.
### Deleting a table or value
Say the user undoes editing the feeds name, and now you want to delete `RSS.feeds.[feedID].editedName` — given the path, youd do this:
ODB.perform {
path.delete()
}
This works on both tables and values. You can also call `delete()` directly on an `ODBTable`, `ODBValueObject`, or `ODBObject`.
### ODBObject
Some functions take or return an `ODBObject`. This is a protocol — the object is either an `ODBTable` or `ODBValueObject`.
There is useful API to be aware of in ODBObject.swift. (But, again, an `ODBObject` reference is valid only with an `ODB.perform` call.)
### ODBTable
You can do some of the same things you can do with an `ODBPath`. You can also get the entire dictionary of `children`, look up any child object, delete all children, add child objects, and more.
### ODBValueObject
You wont use this directly all that often. It wraps an `ODBValue`, which youll use way more often. The useful API for `ODBValueObject` is almost entirely in `ODBObject`.
## Notes
### The root table
The one table you cant delete is the root table — every ODB has a top-level table named `root`. You dont usually specify `root` as the first part of a path, but you could. Its implied.
A path like `["RSS", "feeds"]` is precisely the same as `["root", "RSS", "feeds"]` — theyre interchangeable paths.
### Case-sensitivity
Frontiers object database was case-insensitive: you could refer to the "feeds" table as the "FEeDs" table — it would be the same thing.
While I dont know this for sure, I assume this was because the Macs file system is also case-insensitive. This was considered one of the user-friendly things about Macs.
Were preserved this: this ODB is also case-insensitive. When comparing two keys it always uses the English locale, so that results are predictable no matter what the machines locale actually is. This is something to be aware of.
### Caching and Performance
The database is cached in memory as it is used. A tables children are not read into memory until referenced.
For objects already in memory, reads are fast since theres no need to query the SQLite database.
If this caching becomes a problem in production use — if it tends to use too much memory — well make it smarter.
### Thread safety
Why is it okay to create and refer to `ODBPath` and `ODBValue` objects outside of an `ODB.perform` call, while its not okay with `ODBObject`, `ODBTable`, and `ODBValueObject`?
Because:
`ODBPath` represents a *query* rather than a direct reference. Each time you resolve the object it points to, it recalculates. You can create paths to things that dont exist. The database can change while you hold an `ODBPath` reference, and thats okay: its by design. Just know that you might get back something different every time you refer to `path.object`, `path.value`, and `path.table`.
`ODBValue` is an immutable struct with no connection to the database. Once you get one, it doesnt change, even if the database object it came from changes. (In general these will be short-lived — you just use them for wrapping and unwrapping your apps data.)
On the other hand, `ODBObject`, `ODBTable`, and `ODBValueObject` are direct references to the database. To prevent conflicts and maintain the structure of the database properly, its necessary to use a lock when working with these — thats what `ODB.perform` does.
Say you have a particular table that your app uses a lot. It would seem natural to want to keep a reference to that particular `ODBTable`. Instead, create and keep a reference to an `ODBPath` and refer to `path.table` inside an `ODB.perform` block when you need the table.

View File

@ -0,0 +1,229 @@
//
// DatabaseLookupTable.swift
// RSDatabase
//
// Created by Brent Simmons on 8/5/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
// Implement a lookup table for a many-to-many relationship.
// Example: CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID));
// articleID is objectID; authorID is relatedObjectID.
public final class DatabaseLookupTable {
private let name: String
private let objectIDKey: String
private let relatedObjectIDKey: String
private let relationshipName: String
private let relatedTable: DatabaseRelatedObjectsTable
private var objectIDsWithNoRelatedObjects = Set<String>()
public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseRelatedObjectsTable, relationshipName: String) {
self.name = name
self.objectIDKey = objectIDKey
self.relatedObjectIDKey = relatedObjectIDKey
self.relatedTable = relatedTable
self.relationshipName = relationshipName
}
public func fetchRelatedObjects(for objectIDs: Set<String>, in database: FMDatabase) -> RelatedObjectsMap? {
let objectIDsThatMayHaveRelatedObjects = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
if objectIDsThatMayHaveRelatedObjects.isEmpty {
return nil
}
guard let relatedObjectIDsMap = fetchRelatedObjectIDsMap(objectIDsThatMayHaveRelatedObjects, database) else {
objectIDsWithNoRelatedObjects.formUnion(objectIDsThatMayHaveRelatedObjects)
return nil
}
if let relatedObjects = fetchRelatedObjectsWithIDs(relatedObjectIDsMap.relatedObjectIDs(), database) {
let relatedObjectsMap = RelatedObjectsMap(relatedObjects: relatedObjects, relatedObjectIDsMap: relatedObjectIDsMap)
let objectIDsWithNoFetchedRelatedObjects = objectIDsThatMayHaveRelatedObjects.subtracting(relatedObjectsMap.objectIDs())
objectIDsWithNoRelatedObjects.formUnion(objectIDsWithNoFetchedRelatedObjects)
return relatedObjectsMap
}
return nil
}
public func saveRelatedObjects(for objects: [DatabaseObject], in database: FMDatabase) {
var objectsWithNoRelationships = [DatabaseObject]()
var objectsWithRelationships = [DatabaseObject]()
for object in objects {
if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty {
objectsWithRelationships += [object]
}
else {
objectsWithNoRelationships += [object]
}
}
removeRelationships(for: objectsWithNoRelationships, database)
updateRelationships(for: objectsWithRelationships, database)
objectIDsWithNoRelatedObjects.formUnion(objectsWithNoRelationships.databaseIDs())
objectIDsWithNoRelatedObjects.subtract(objectsWithRelationships.databaseIDs())
}
}
// MARK: - Private
private extension DatabaseLookupTable {
// MARK: Removing
func removeRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
let objectIDs = objects.databaseIDs()
let objectIDsToRemove = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
if objectIDsToRemove.isEmpty {
return
}
database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name)
}
func deleteLookups(for objectID: String, _ relatedObjectIDs: Set<String>, _ database: FMDatabase) {
guard !relatedObjectIDs.isEmpty else {
assertionFailure("deleteLookups: expected non-empty relatedObjectIDs")
return
}
// delete from authorLookup where articleID=? and authorID in (?,?,?)
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(relatedObjectIDs.count))!
let sql = "delete from \(name) where \(objectIDKey)=? and \(relatedObjectIDKey) in \(placeholders)"
let parameters: [Any] = [objectID] + Array(relatedObjectIDs)
let _ = database.executeUpdate(sql, withArgumentsIn: parameters)
}
// MARK: Saving/Updating
func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
if objects.isEmpty {
return
}
if let lookupTable = fetchRelatedObjectIDsMap(objects.databaseIDs(), database) {
for object in objects {
syncRelatedObjectsAndLookupTable(object, lookupTable, database)
}
}
// Save the actual related objects.
let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objects)
if relatedObjectsToSave.isEmpty {
assertionFailure("updateRelationships: expected relatedObjectsToSave would not be empty. This should be unreachable.")
return
}
relatedTable.save(relatedObjectsToSave, in: database)
}
func uniqueArrayOfRelatedObjects(with objects: [DatabaseObject]) -> [DatabaseObject] {
// Cant create a Set, because we cant make a Set<DatabaseObject>, because protocol-conforming objects cant be made Hashable or even Equatable.
// We still want the array to include only one copy of each object, but we have to do it the slow way. Instruments will tell us if this is a performance problem.
var relatedObjectsUniqueArray = [DatabaseObject]()
for object in objects {
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
assertionFailure("uniqueArrayOfRelatedObjects: expected every object to have related objects.")
continue
}
for relatedObject in relatedObjects {
if !relatedObjectsUniqueArray.includesObjectWithDatabaseID(relatedObject.databaseID) {
relatedObjectsUniqueArray += [relatedObject]
}
}
}
return relatedObjectsUniqueArray
}
func syncRelatedObjectsAndLookupTable(_ object: DatabaseObject, _ lookupTable: RelatedObjectIDsMap, _ database: FMDatabase) {
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
assertionFailure("syncRelatedObjectsAndLookupTable should be called only on objects with related objects.")
return
}
let relatedObjectIDs = relatedObjects.databaseIDs()
let lookupTableRelatedObjectIDs = lookupTable[object.databaseID] ?? Set<String>()
let relatedObjectIDsToDelete = lookupTableRelatedObjectIDs.subtracting(relatedObjectIDs)
if !relatedObjectIDsToDelete.isEmpty {
deleteLookups(for: object.databaseID, relatedObjectIDsToDelete, database)
}
let relatedObjectIDsToSave = relatedObjectIDs.subtracting(lookupTableRelatedObjectIDs)
if !relatedObjectIDsToSave.isEmpty {
saveLookups(for: object.databaseID, relatedObjectIDsToSave, database)
}
}
func saveLookups(for objectID: String, _ relatedObjectIDs: Set<String>, _ database: FMDatabase) {
for relatedObjectID in relatedObjectIDs {
let d: [NSObject: Any] = [(objectIDKey as NSString): objectID, (relatedObjectIDKey as NSString): relatedObjectID]
let _ = database.rs_insertRow(with: d, insertType: .orIgnore, tableName: name)
}
}
// MARK: Fetching
func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set<String>, _ database: FMDatabase) -> [DatabaseObject]? {
guard let relatedObjects = relatedTable.fetchObjectsWithIDs(relatedObjectIDs, in: database), !relatedObjects.isEmpty else {
return nil
}
return relatedObjects
}
func fetchRelatedObjectIDsMap(_ objectIDs: Set<String>, _ database: FMDatabase) -> RelatedObjectIDsMap? {
guard let lookupValues = fetchLookupValues(objectIDs, database) else {
return nil
}
return RelatedObjectIDsMap(lookupValues: lookupValues)
}
func fetchLookupValues(_ objectIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else {
return nil
}
return lookupValuesWithResultSet(resultSet)
}
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
return resultSet.mapToSet(lookupValueWithRow)
}
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
guard let objectID = row.string(forColumn: objectIDKey) else {
return nil
}
guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else {
return nil
}
return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID)
}
}

View File

@ -0,0 +1,83 @@
//
// DatabaseRelatedObjectsTable.swift
// RSDatabase
//
// Created by Brent Simmons on 9/2/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
// Protocol for a database table for related objects  authors and attachments in NetNewsWire, for instance.
public protocol DatabaseRelatedObjectsTable: DatabaseTable {
var databaseIDKey: String { get}
var cache: DatabaseObjectCache { get }
func fetchObjectsWithIDs(_ databaseIDs: Set<String>, in database: FMDatabase) -> [DatabaseObject]?
func objectsWithResultSet(_ resultSet: FMResultSet) -> [DatabaseObject]
func objectWithRow(_ row: FMResultSet) -> DatabaseObject?
func save(_ objects: [DatabaseObject], in database: FMDatabase)
}
public extension DatabaseRelatedObjectsTable {
// MARK: Default implementations
func fetchObjectsWithIDs(_ databaseIDs: Set<String>, in database: FMDatabase) -> [DatabaseObject]? {
if databaseIDs.isEmpty {
return nil
}
var cachedObjects = [DatabaseObject]()
var databaseIDsToFetch = Set<String>()
for databaseID in databaseIDs {
if let cachedObject = cache[databaseID] {
cachedObjects += [cachedObject]
}
else {
databaseIDsToFetch.insert(databaseID)
}
}
if databaseIDsToFetch.isEmpty {
return cachedObjects
}
guard let resultSet = selectRowsWhere(key: databaseIDKey, inValues: Array(databaseIDsToFetch), in: database) else {
return cachedObjects
}
let fetchedDatabaseObjects = objectsWithResultSet(resultSet)
cache.add(fetchedDatabaseObjects)
return cachedObjects + fetchedDatabaseObjects
}
func objectsWithResultSet(_ resultSet: FMResultSet) -> [DatabaseObject] {
return resultSet.compactMap(objectWithRow)
}
func save(_ objects: [DatabaseObject], in database: FMDatabase) {
// Objects in cache must already exist in database. Filter them out.
let objectsToSave = objects.filter { (object) -> Bool in
if let _ = cache[object.databaseID] {
return false
}
return true
}
cache.add(objectsToSave)
if let databaseDictionaries = objectsToSave.databaseDictionaries() {
insertRows(databaseDictionaries, insertType: .orIgnore, in: database)
}
}
}

View File

@ -0,0 +1,63 @@
//
// RelatedObjectIDsMap.swift
// RSDatabase
//
// Created by Brent Simmons on 9/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Maps objectIDs to Set<String> where the Strings are relatedObjectIDs.
struct RelatedObjectIDsMap {
private let dictionary: [String: Set<String>] // objectID: Set<relatedObjectID>
init(dictionary: [String: Set<String>]) {
self.dictionary = dictionary
}
init(lookupValues: Set<LookupValue>) {
var d = [String: Set<String>]()
for lookupValue in lookupValues {
let objectID = lookupValue.objectID
let relatedObjectID: String = lookupValue.relatedObjectID
if d[objectID] == nil {
d[objectID] = Set([relatedObjectID])
}
else {
d[objectID]!.insert(relatedObjectID)
}
}
self.init(dictionary: d)
}
func objectIDs() -> Set<String> {
return Set(dictionary.keys)
}
func relatedObjectIDs() -> Set<String> {
var ids = Set<String>()
for (_, relatedObjectIDs) in dictionary {
ids.formUnion(relatedObjectIDs)
}
return ids
}
subscript(_ objectID: String) -> Set<String>? {
return dictionary[objectID]
}
}
struct LookupValue: Hashable {
let objectID: String
let relatedObjectID: String
}

View File

@ -0,0 +1,44 @@
//
// RelatedObjectsMap.swift
// RSDatabase
//
// Created by Brent Simmons on 9/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Map objectID to [DatabaseObject] (related objects).
// Its used as the return value for DatabaseLookupTable.fetchRelatedObjects.
public struct RelatedObjectsMap {
private let dictionary: [String: [DatabaseObject]] // objectID: relatedObjects
init(relatedObjects: [DatabaseObject], relatedObjectIDsMap: RelatedObjectIDsMap) {
var d = [String: [DatabaseObject]]()
let relatedObjectsDictionary = relatedObjects.dictionary()
for objectID in relatedObjectIDsMap.objectIDs() {
if let relatedObjectIDs = relatedObjectIDsMap[objectID] {
let relatedObjects = relatedObjectIDs.compactMap{ relatedObjectsDictionary[$0] }
if !relatedObjects.isEmpty {
d[objectID] = relatedObjects
}
}
}
self.dictionary = d
}
public func objectIDs() -> Set<String> {
return Set(dictionary.keys)
}
public subscript(_ objectID: String) -> [DatabaseObject]? {
return dictionary[objectID]
}
}

View File

@ -0,0 +1,83 @@
//
// FMDatabase+QSKit.h
// RSDatabase
//
// Created by Brent Simmons on 3/3/14.
// Copyright (c) 2014 Ranchero Software, LLC. All rights reserved.
//
#import "FMDatabase.h"
@import Foundation;
typedef NS_ENUM(NSInteger, RSDatabaseInsertType) {
RSDatabaseInsertNormal,
RSDatabaseInsertOrReplace,
RSDatabaseInsertOrIgnore
};
NS_ASSUME_NONNULL_BEGIN
@interface FMDatabase (RSExtras)
// Keys and table names are assumed to be trusted. Values are not.
// delete from tableName where key in (?, ?, ?)
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName;
// delete from tableName where key=?
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// select * from tableName where key in (?, ?, ?)
- (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName;
// select * from tableName where key = ?
- (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// select * from tableName where key = ? limit 1
- (FMResultSet * _Nullable)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// select * from tableName
- (FMResultSet * _Nullable)rs_selectAllRows:(NSString *)tableName;
// select key from tableName;
- (FMResultSet * _Nullable)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName;
// select 1 from tableName where key = value limit 1;
- (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName;
// select 1 from tableName limit 1;
- (BOOL)rs_tableIsEmpty:(NSString *)tableName;
// update tableName set key1=?, key2=? where key = value
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// update tableName set key1=?, key2=? where key in (?, ?, ?)
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName;
// update tableName set valueKey=? where where key in (?, ?, ?)
- (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName;
// insert (or replace, or ignore) into tablename (key1, key2) values (val1, val2)
- (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,180 @@
//
// FMDatabase+QSKit.m
// RSDatabase
//
// Created by Brent Simmons on 3/3/14.
// Copyright (c) 2014 Ranchero Software, LLC. All rights reserved.
//
#import "FMDatabase+RSExtras.h"
#import "NSString+RSDatabase.h"
#define LOG_SQL 0
static void logSQL(NSString *sql) {
#if LOG_SQL
NSLog(@"sql: %@", sql);
#endif
}
@implementation FMDatabase (RSExtras)
#pragma mark - Deleting
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName {
if ([values count] < 1) {
return YES;
}
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ in %@", tableName, key, placeholders];
logSQL(sql);
return [self executeUpdate:sql withArgumentsInArray:values];
}
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ = ?", tableName, key];
logSQL(sql);
return [self executeUpdate:sql, value];
}
#pragma mark - Selecting
- (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName {
NSMutableString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ in ", tableName, key];
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
[sql appendString:placeholders];
logSQL(sql);
return [self executeQuery:sql withArgumentsInArray:values];
}
- (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ?", tableName, key];
logSQL(sql);
return [self executeQuery:sql, value];
}
- (FMResultSet *)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ? limit 1", tableName, key];
logSQL(sql);
return [self executeQuery:sql, value];
}
- (FMResultSet *)rs_selectAllRows:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName];
logSQL(sql);
return [self executeQuery:sql];
}
- (FMResultSet *)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select %@ from %@", key, tableName];
logSQL(sql);
return [self executeQuery:sql];
}
- (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select 1 from %@ where %@ = ? limit 1;", tableName, key];
logSQL(sql);
FMResultSet *rs = [self executeQuery:sql, value];
return [rs next];
}
- (BOOL)rs_tableIsEmpty:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select 1 from %@ limit 1;", tableName];
logSQL(sql);
FMResultSet *rs = [self executeQuery:sql];
BOOL isEmpty = YES;
while ([rs next]) {
isEmpty = NO;
}
return isEmpty;
}
#pragma mark - Updating
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
return [self rs_updateRowsWithDictionary:d whereKey:key inValues:@[value] tableName:tableName];
}
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName {
NSMutableArray *keys = [NSMutableArray new];
NSMutableArray *values = [NSMutableArray new];
[d enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[keys addObject:key];
[values addObject:obj];
}];
NSString *keyPlaceholders = [NSString rs_SQLKeyPlaceholderPairsWithKeys:keys];
NSString *keyValuesPlaceholder = [NSString rs_SQLValueListWithPlaceholders:keyValues.count];
NSString *sql = [NSString stringWithFormat:@"update %@ set %@ where %@ in %@", tableName, keyPlaceholders, key, keyValuesPlaceholder];
NSMutableArray *parameters = values;
[parameters addObjectsFromArray:keyValues];
logSQL(sql);
return [self executeUpdate:sql withArgumentsInArray:parameters];
}
- (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName {
NSDictionary *d = @{valueKey: value};
return [self rs_updateRowsWithDictionary:d whereKey:key inValues:keyValues tableName:tableName];
}
#pragma mark - Saving
- (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName {
NSArray *keys = d.allKeys;
NSArray *values = [d objectsForKeys:keys notFoundMarker:[NSNull null]];
NSString *sqlKeysList = [NSString rs_SQLKeysListWithArray:keys];
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
NSString *sqlBeginning = @"insert into ";
if (insertType == RSDatabaseInsertOrReplace) {
sqlBeginning = @"insert or replace into ";
}
else if (insertType == RSDatabaseInsertOrIgnore) {
sqlBeginning = @"insert or ignore into ";
}
NSString *sql = [NSString stringWithFormat:@"%@ %@ %@ values %@", sqlBeginning, tableName, sqlKeysList, placeholders];
logSQL(sql);
return [self executeUpdate:sql withArgumentsInArray:values];
}
@end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,281 @@
//
// FMDatabaseAdditions.h
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FMDatabase.h"
/** Category of additions for `<FMDatabase>` class.
### See also
- `<FMDatabase>`
*/
@interface FMDatabase (FMDatabaseAdditions)
///----------------------------------------
/// @name Return results of SQL to variable
///----------------------------------------
/** Return `int` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `int` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (int)intForQuery:(NSString*)query, ...;
/** Return `long` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `long` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (long)longForQuery:(NSString*)query, ...;
/** Return `BOOL` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `BOOL` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (BOOL)boolForQuery:(NSString*)query, ...;
/** Return `double` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `double` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (double)doubleForQuery:(NSString*)query, ...;
/** Return `NSString` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `NSString` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (NSString*)stringForQuery:(NSString*)query, ...;
/** Return `NSData` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `NSData` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (NSData*)dataForQuery:(NSString*)query, ...;
/** Return `NSDate` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `NSDate` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (NSDate*)dateForQuery:(NSString*)query, ...;
// Notice that there's no dataNoCopyForQuery:.
// That would be a bad idea, because we close out the result set, and then what
// happens to the data that we just didn't copy? Who knows, not I.
///--------------------------------
/// @name Schema related operations
///--------------------------------
/** Does table exist in database?
@param tableName The name of the table being looked for.
@return `YES` if table found; `NO` if not found.
*/
- (BOOL)tableExists:(NSString*)tableName;
/** The schema of the database.
This will be the schema for the entire database. For each entity, each row of the result set will include the following fields:
- `type` - The type of entity (e.g. table, index, view, or trigger)
- `name` - The name of the object
- `tbl_name` - The name of the table to which the object references
- `rootpage` - The page number of the root b-tree page for tables and indices
- `sql` - The SQL that created the entity
@return `FMResultSet` of schema; `nil` on error.
@see [SQLite File Format](http://www.sqlite.org/fileformat.html)
*/
- (FMResultSet*)getSchema;
/** The schema of the database.
This will be the schema for a particular table as report by SQLite `PRAGMA`, for example:
PRAGMA table_info('employees')
This will report:
- `cid` - The column ID number
- `name` - The name of the column
- `type` - The data type specified for the column
- `notnull` - whether the field is defined as NOT NULL (i.e. values required)
- `dflt_value` - The default value for the column
- `pk` - Whether the field is part of the primary key of the table
@param tableName The name of the table for whom the schema will be returned.
@return `FMResultSet` of schema; `nil` on error.
@see [table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
*/
- (FMResultSet*)getTableSchema:(NSString*)tableName;
/** Test to see if particular column exists for particular table in database
@param columnName The name of the column.
@param tableName The name of the table.
@return `YES` if column exists in table in question; `NO` otherwise.
*/
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName;
/** Test to see if particular column exists for particular table in database
@param columnName The name of the column.
@param tableName The name of the table.
@return `YES` if column exists in table in question; `NO` otherwise.
@see columnExists:inTableWithName:
@warning Deprecated - use `<columnExists:inTableWithName:>` instead.
*/
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated));
/** Validate SQL statement
This validates SQL statement by performing `sqlite3_prepare_v2`, but not returning the results, but instead immediately calling `sqlite3_finalize`.
@param sql The SQL statement being validated.
@param error This is a pointer to a `NSError` object that will receive the autoreleased `NSError` object if there was any error. If this is `nil`, no `NSError` result will be returned.
@return `YES` if validation succeeded without incident; `NO` otherwise.
*/
- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error;
#if SQLITE_VERSION_NUMBER >= 3007017
///-----------------------------------
/// @name Application identifier tasks
///-----------------------------------
/** Retrieve application ID
@return The `uint32_t` numeric value of the application ID.
@see setApplicationID:
*/
- (uint32_t)applicationID;
/** Set the application ID
@param appID The `uint32_t` numeric value of the application ID.
@see applicationID
*/
- (void)setApplicationID:(uint32_t)appID;
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
/** Retrieve application ID string
@return The `NSString` value of the application ID.
@see setApplicationIDString:
*/
- (NSString*)applicationIDString;
/** Set the application ID string
@param string The `NSString` value of the application ID.
@see applicationIDString
*/
- (void)setApplicationIDString:(NSString*)string;
#endif
#endif
///-----------------------------------
/// @name user version identifier tasks
///-----------------------------------
/** Retrieve user version
@return The `uint32_t` numeric value of the user version.
@see setUserVersion:
*/
- (uint32_t)userVersion;
/** Set the user-version
@param version The `uint32_t` numeric value of the user version.
@see userVersion
*/
- (void)setUserVersion:(uint32_t)version;
@end

View File

@ -0,0 +1,225 @@
//
// FMDatabaseAdditions.m
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#import "TargetConditionals.h"
#import "sqlite3.h"
@interface FMDatabase (PrivateStuff)
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args;
@end
@implementation FMDatabase (FMDatabaseAdditions)
#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \
va_list args; \
va_start(args, query); \
FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \
va_end(args); \
if (![resultSet next]) { return (type)0; } \
type ret = [resultSet sel:0]; \
[resultSet close]; \
[resultSet setParentDB:nil]; \
return ret;
- (NSString*)stringForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex);
}
- (int)intForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex);
}
- (long)longForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex);
}
- (BOOL)boolForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex);
}
- (double)doubleForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex);
}
- (NSData*)dataForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex);
}
- (NSDate*)dateForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex);
}
- (BOOL)tableExists:(NSString*)tableName {
tableName = [tableName lowercaseString];
FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
//if at least one next exists, table exists
BOOL returnBool = [rs next];
//close and free object
[rs close];
return returnBool;
}
/*
get table with list of tables: result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
check if table exist in database (patch from OZLB)
*/
- (FMResultSet*)getSchema {
//result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
FMResultSet *rs = [self executeQuery:@"SELECT type, name, tbl_name, rootpage, sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type != 'meta' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name"];
return rs;
}
/*
get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
*/
- (FMResultSet*)getTableSchema:(NSString*)tableName {
//result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
FMResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"pragma table_info('%@')", tableName]];
return rs;
}
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName {
BOOL returnBool = NO;
tableName = [tableName lowercaseString];
columnName = [columnName lowercaseString];
FMResultSet *rs = [self getTableSchema:tableName];
//check if column is present in table schema
while ([rs next]) {
if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString:columnName]) {
returnBool = YES;
break;
}
}
//If this is not done FMDatabase instance stays out of pool
[rs close];
return returnBool;
}
#if SQLITE_VERSION_NUMBER >= 3007017
- (uint32_t)applicationID {
uint32_t r = 0;
FMResultSet *rs = [self executeQuery:@"pragma application_id"];
if ([rs next]) {
r = (uint32_t)[rs longLongIntForColumnIndex:0];
}
[rs close];
return r;
}
- (void)setApplicationID:(uint32_t)appID {
NSString *query = [NSString stringWithFormat:@"pragma application_id=%d", appID];
FMResultSet *rs = [self executeQuery:query];
[rs next];
[rs close];
}
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
- (NSString*)applicationIDString {
NSString *s = NSFileTypeForHFSTypeCode([self applicationID]);
assert([s length] == 6);
s = [s substringWithRange:NSMakeRange(1, 4)];
return s;
}
- (void)setApplicationIDString:(NSString*)s {
if ([s length] != 4) {
NSLog(@"setApplicationIDString: string passed is not exactly 4 chars long. (was %ld)", [s length]);
}
[self setApplicationID:NSHFSTypeCodeFromFileType([NSString stringWithFormat:@"'%@'", s])];
}
#endif
#endif
- (uint32_t)userVersion {
uint32_t r = 0;
FMResultSet *rs = [self executeQuery:@"pragma user_version"];
if ([rs next]) {
r = (uint32_t)[rs longLongIntForColumnIndex:0];
}
[rs close];
return r;
}
- (void)setUserVersion:(uint32_t)version {
NSString *query = [NSString stringWithFormat:@"pragma user_version = %d", version];
FMResultSet *rs = [self executeQuery:query];
[rs next];
[rs close];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)) {
return [self columnExists:columnName inTableWithName:tableName];
}
#pragma clang diagnostic pop
- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error {
sqlite3_stmt *pStmt = NULL;
BOOL validationSucceeded = YES;
int rc = sqlite3_prepare_v2([self sqliteHandle], [sql UTF8String], -1, &pStmt, 0);
if (rc != SQLITE_OK) {
validationSucceeded = NO;
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain
code:[self lastErrorCode]
userInfo:[NSDictionary dictionaryWithObject:[self lastErrorMessage]
forKey:NSLocalizedDescriptionKey]];
}
}
sqlite3_finalize(pStmt);
return validationSucceeded;
}
@end

View File

@ -0,0 +1,23 @@
//
// FMResultSet+RSExtras.h
// RSDatabase
//
// Created by Brent Simmons on 2/19/13.
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
//
#import "FMResultSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface FMResultSet (RSExtras)
- (NSArray *)rs_arrayForSingleColumnResultSet; // Doesn't handle dates.
- (NSSet *)rs_setForSingleColumnResultSet;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,51 @@
//
// FMResultSet+RSExtras.m
// RSDatabase
//
// Created by Brent Simmons on 2/19/13.
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
//
#import "FMResultSet+RSExtras.h"
@implementation FMResultSet (RSExtras)
- (id)valueForKey:(NSString *)key {
if ([key containsString:@"Date"] || [key containsString:@"date"]) {
return [self dateForColumn:key];
}
return [self objectForColumnName:key];
}
- (NSArray *)rs_arrayForSingleColumnResultSet {
NSMutableArray *results = [NSMutableArray new];
while ([self next]) {
id oneObject = [self objectForColumnIndex:0];
[results addObject:oneObject];
}
return [results copy];
}
- (NSSet *)rs_setForSingleColumnResultSet {
NSMutableSet *results = [NSMutableSet new];
while ([self next]) {
id oneObject = [self objectForColumnIndex:0];
[results addObject:oneObject];
}
return [results copy];
}
@end

View File

@ -0,0 +1,469 @@
#import <Foundation/Foundation.h>
//#import "sqlite3.h"
#ifndef __has_feature // Optional.
#define __has_feature(x) 0 // Compatibility with non-clang compilers.
#endif
#ifndef NS_RETURNS_NOT_RETAINED
#if __has_feature(attribute_ns_returns_not_retained)
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
#else
#define NS_RETURNS_NOT_RETAINED
#endif
#endif
@class FMDatabase;
@class FMStatement;
/** Represents the results of executing a query on an `<FMDatabase>`.
### See also
- `<FMDatabase>`
*/
@interface FMResultSet : NSObject {
FMDatabase *_parentDB;
FMStatement *_statement;
NSString *_query;
NSMutableDictionary *_columnNameToIndexMap;
}
///-----------------
/// @name Properties
///-----------------
/** Executed query */
@property (atomic, retain) NSString *query;
/** `NSMutableDictionary` mapping column names to numeric index */
@property (readonly) NSMutableDictionary *columnNameToIndexMap;
/** `FMStatement` used by result set. */
@property (atomic, retain) FMStatement *statement;
///------------------------------------
/// @name Creating and closing database
///------------------------------------
/** Create result set from `<FMStatement>`
@param statement A `<FMStatement>` to be performed
@param aDB A `<FMDatabase>` to be used
@return A `FMResultSet` on success; `nil` on failure
*/
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB;
/** Close result set */
- (void)close;
- (void)setParentDB:(FMDatabase *)newDb;
///---------------------------------------
/// @name Iterating through the result set
///---------------------------------------
/** Retrieve next row for result set.
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
@return `YES` if row successfully retrieved; `NO` if end of result set reached
@see hasAnotherRow
*/
- (BOOL)next;
/** Retrieve next row for result set.
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
@param outErr A 'NSError' object to receive any error object (if any).
@return 'YES' if row successfully retrieved; 'NO' if end of result set reached
@see hasAnotherRow
*/
- (BOOL)nextWithError:(NSError **)outErr;
/** Did the last call to `<next>` succeed in retrieving another row?
@return `YES` if the last call to `<next>` succeeded in retrieving another record; `NO` if not.
@see next
@warning The `hasAnotherRow` method must follow a call to `<next>`. If the previous database interaction was something other than a call to `next`, then this method may return `NO`, whether there is another row of data or not.
*/
- (BOOL)hasAnotherRow;
///---------------------------------------------
/// @name Retrieving information from result set
///---------------------------------------------
/** How many columns in result set
@return Integer value of the number of columns.
*/
- (int)columnCount;
/** Column index for column name
@param columnName `NSString` value of the name of the column.
@return Zero-based index for column.
*/
- (int)columnIndexForName:(NSString*)columnName;
/** Column name for column index
@param columnIdx Zero-based index for column.
@return columnName `NSString` value of the name of the column.
*/
- (NSString*)columnNameForIndex:(int)columnIdx;
/** Result set integer value for column.
@param columnName `NSString` value of the name of the column.
@return `int` value of the result set's column.
*/
- (int)intForColumn:(NSString*)columnName;
/** Result set integer value for column.
@param columnIdx Zero-based index for column.
@return `int` value of the result set's column.
*/
- (int)intForColumnIndex:(int)columnIdx;
/** Result set `long` value for column.
@param columnName `NSString` value of the name of the column.
@return `long` value of the result set's column.
*/
- (long)longForColumn:(NSString*)columnName;
/** Result set long value for column.
@param columnIdx Zero-based index for column.
@return `long` value of the result set's column.
*/
- (long)longForColumnIndex:(int)columnIdx;
/** Result set `long long int` value for column.
@param columnName `NSString` value of the name of the column.
@return `long long int` value of the result set's column.
*/
- (long long int)longLongIntForColumn:(NSString*)columnName;
/** Result set `long long int` value for column.
@param columnIdx Zero-based index for column.
@return `long long int` value of the result set's column.
*/
- (long long int)longLongIntForColumnIndex:(int)columnIdx;
/** Result set `unsigned long long int` value for column.
@param columnName `NSString` value of the name of the column.
@return `unsigned long long int` value of the result set's column.
*/
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName;
/** Result set `unsigned long long int` value for column.
@param columnIdx Zero-based index for column.
@return `unsigned long long int` value of the result set's column.
*/
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx;
/** Result set `BOOL` value for column.
@param columnName `NSString` value of the name of the column.
@return `BOOL` value of the result set's column.
*/
- (BOOL)boolForColumn:(NSString*)columnName;
/** Result set `BOOL` value for column.
@param columnIdx Zero-based index for column.
@return `BOOL` value of the result set's column.
*/
- (BOOL)boolForColumnIndex:(int)columnIdx;
/** Result set `double` value for column.
@param columnName `NSString` value of the name of the column.
@return `double` value of the result set's column.
*/
- (double)doubleForColumn:(NSString*)columnName;
/** Result set `double` value for column.
@param columnIdx Zero-based index for column.
@return `double` value of the result set's column.
*/
- (double)doubleForColumnIndex:(int)columnIdx;
/** Result set `NSString` value for column.
@param columnName `NSString` value of the name of the column.
@return `NSString` value of the result set's column.
*/
- (NSString*)stringForColumn:(NSString*)columnName;
/** Result set `NSString` value for column.
@param columnIdx Zero-based index for column.
@return `NSString` value of the result set's column.
*/
- (NSString*)stringForColumnIndex:(int)columnIdx;
/** Result set `NSDate` value for column.
@param columnName `NSString` value of the name of the column.
@return `NSDate` value of the result set's column.
*/
- (NSDate*)dateForColumn:(NSString*)columnName;
/** Result set `NSDate` value for column.
@param columnIdx Zero-based index for column.
@return `NSDate` value of the result set's column.
*/
- (NSDate*)dateForColumnIndex:(int)columnIdx;
/** Result set `NSData` value for column.
This is useful when storing binary data in table (such as image or the like).
@param columnName `NSString` value of the name of the column.
@return `NSData` value of the result set's column.
*/
- (NSData*)dataForColumn:(NSString*)columnName;
/** Result set `NSData` value for column.
@param columnIdx Zero-based index for column.
@return `NSData` value of the result set's column.
*/
- (NSData*)dataForColumnIndex:(int)columnIdx;
/** Result set `(const unsigned char *)` value for column.
@param columnName `NSString` value of the name of the column.
@return `(const unsigned char *)` value of the result set's column.
*/
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName;
/** Result set `(const unsigned char *)` value for column.
@param columnIdx Zero-based index for column.
@return `(const unsigned char *)` value of the result set's column.
*/
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx;
/** Result set object for column.
@param columnName `NSString` value of the name of the column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
@see objectForKeyedSubscript:
*/
- (id)objectForColumnName:(NSString*)columnName;
/** Result set object for column.
@param columnIdx Zero-based index for column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
@see objectAtIndexedSubscript:
*/
- (id)objectForColumnIndex:(int)columnIdx;
/** Result set object for column.
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
id result = rs[@"employee_name"];
This simplified syntax is equivalent to calling:
id result = [rs objectForKeyedSubscript:@"employee_name"];
which is, it turns out, equivalent to calling:
id result = [rs objectForColumnName:@"employee_name"];
@param columnName `NSString` value of the name of the column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
*/
- (id)objectForKeyedSubscript:(NSString *)columnName;
/** Result set object for column.
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
id result = rs[0];
This simplified syntax is equivalent to calling:
id result = [rs objectForKeyedSubscript:0];
which is, it turns out, equivalent to calling:
id result = [rs objectForColumnName:0];
@param columnIdx Zero-based index for column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
*/
- (id)objectAtIndexedSubscript:(int)columnIdx;
/** Result set `NSData` value for column.
@param columnName `NSString` value of the name of the column.
@return `NSData` value of the result set's column.
@warning If you are going to use this data after you iterate over the next row, or after you close the
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
If you don't, you're going to be in a world of hurt when you try and use the data.
*/
- (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED;
/** Result set `NSData` value for column.
@param columnIdx Zero-based index for column.
@return `NSData` value of the result set's column.
@warning If you are going to use this data after you iterate over the next row, or after you close the
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
If you don't, you're going to be in a world of hurt when you try and use the data.
*/
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED;
/** Is the column `NULL`?
@param columnIdx Zero-based index for column.
@return `YES` if column is `NULL`; `NO` if not `NULL`.
*/
- (BOOL)columnIndexIsNull:(int)columnIdx;
/** Is the column `NULL`?
@param columnName `NSString` value of the name of the column.
@return `YES` if column is `NULL`; `NO` if not `NULL`.
*/
- (BOOL)columnIsNull:(NSString*)columnName;
/** Returns a dictionary of the row results mapped to case sensitive keys of the column names.
@returns `NSDictionary` of the row results.
@warning The keys to the dictionary are case sensitive of the column names.
*/
- (NSDictionary*)resultDictionary;
/** Returns a dictionary of the row results
@see resultDictionary
@warning **Deprecated**: Please use `<resultDictionary>` instead. Also, beware that `<resultDictionary>` is case sensitive!
*/
- (NSDictionary*)resultDict __attribute__ ((deprecated));
///-----------------------------
/// @name Key value coding magic
///-----------------------------
/** Performs `setValue` to yield support for key value observing.
@param object The object for which the values will be set. This is the key-value-coding compliant object that you might, for example, observe.
*/
- (void)kvcMagic:(id)object;
@end

View File

@ -0,0 +1,454 @@
#import "FMResultSet.h"
#import "FMDatabase.h"
#import "unistd.h"
#import "sqlite3.h"
@interface FMDatabase ()
- (void)resultSetDidClose:(FMResultSet *)resultSet;
@end
@interface FMResultSet ()
@property (nonatomic, readonly) NSDictionary *columnNameToIndexMapNonLowercased;
@end
@implementation FMResultSet
@synthesize query=_query;
@synthesize statement=_statement;
@synthesize columnNameToIndexMapNonLowercased = _columnNameToIndexMapNonLowercased;
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB {
FMResultSet *rs = [[FMResultSet alloc] init];
[rs setStatement:statement];
[rs setParentDB:aDB];
NSParameterAssert(![statement inUse]);
[statement setInUse:YES]; // weak reference
return FMDBReturnAutoreleased(rs);
}
- (void)dealloc {
[self close];
FMDBRelease(_query);
_query = nil;
FMDBRelease(_columnNameToIndexMap);
_columnNameToIndexMap = nil;
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}
- (void)close {
[_statement reset];
FMDBRelease(_statement);
_statement = nil;
// we don't need this anymore... (i think)
//[_parentDB setInUse:NO];
[_parentDB resultSetDidClose:self];
_parentDB = nil;
}
- (int)columnCount {
return sqlite3_column_count([_statement statement]);
}
- (NSMutableDictionary *)columnNameToIndexMap {
if (!_columnNameToIndexMap) {
NSDictionary *nonLowercasedMap = self.columnNameToIndexMapNonLowercased;
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:nonLowercasedMap.count];
for (NSString *key in nonLowercasedMap.allKeys) {
[d setObject:nonLowercasedMap[key] forKey:[self _lowercaseString:key]];
}
_columnNameToIndexMap = d;
}
return _columnNameToIndexMap;
}
- (NSDictionary *)columnNameToIndexMapNonLowercased {
if (!_columnNameToIndexMapNonLowercased) {
int columnCount = sqlite3_column_count([_statement statement]);
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
[d setObject:[NSNumber numberWithInt:columnIdx]
forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
}
_columnNameToIndexMapNonLowercased = d;
}
return _columnNameToIndexMapNonLowercased;
}
- (void)kvcMagic:(id)object {
int columnCount = sqlite3_column_count([_statement statement]);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
// check for a null row
if (c) {
NSString *s = [NSString stringWithUTF8String:c];
[object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (NSDictionary*)resultDict {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
NSEnumerator *columnNames = [[self columnNameToIndexMap] keyEnumerator];
NSString *columnName = nil;
while ((columnName = [columnNames nextObject])) {
id objectValue = [self objectForColumnName:columnName];
[dict setObject:objectValue forKey:columnName];
}
return FMDBReturnAutoreleased([dict copy]);
}
else {
NSLog(@"Warning: There seem to be no columns in this set.");
}
return nil;
}
#pragma clang diagnostic pop
- (NSDictionary*)resultDictionary {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count([_statement statement]);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)];
id objectValue = [self objectForColumnIndex:columnIdx];
[dict setObject:objectValue forKey:columnName];
}
return dict;
}
else {
NSLog(@"Warning: There seem to be no columns in this set.");
}
return nil;
}
- (BOOL)next {
return [self nextWithError:nil];
}
- (BOOL)nextWithError:(NSError **)outErr {
int rc = sqlite3_step([_statement statement]);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
NSLog(@"Database busy");
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
// all is well, let's return.
}
else if (SQLITE_ERROR == rc) {
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_MISUSE == rc) {
// uh oh.
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
if (_parentDB) {
*outErr = [_parentDB lastError];
}
else {
// If 'next' or 'nextWithError' is called after the result set is closed,
// we need to return the appropriate error.
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
*outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
}
}
}
else {
// wtf?
NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
if (rc != SQLITE_ROW) {
[self close];
}
return (rc == SQLITE_ROW);
}
- (BOOL)hasAnotherRow {
return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW;
}
- (int)columnIndexForName:(NSString*)columnName {
NSNumber *n = self.columnNameToIndexMapNonLowercased[columnName];
if (!n) {
columnName = [self _lowercaseString:columnName];
n = [[self columnNameToIndexMap] objectForKey:columnName];
}
if (n) {
return [n intValue];
}
NSLog(@"Warning: I could not find the column named '%@'.", columnName);
return -1;
}
- (int)intForColumn:(NSString*)columnName {
return [self intForColumnIndex:[self columnIndexForName:columnName]];
}
- (int)intForColumnIndex:(int)columnIdx {
return sqlite3_column_int([_statement statement], columnIdx);
}
- (long)longForColumn:(NSString*)columnName {
return [self longForColumnIndex:[self columnIndexForName:columnName]];
}
- (long)longForColumnIndex:(int)columnIdx {
return (long)sqlite3_column_int64([_statement statement], columnIdx);
}
- (long long int)longLongIntForColumn:(NSString*)columnName {
return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]];
}
- (long long int)longLongIntForColumnIndex:(int)columnIdx {
return sqlite3_column_int64([_statement statement], columnIdx);
}
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName {
return [self unsignedLongLongIntForColumnIndex:[self columnIndexForName:columnName]];
}
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx {
return (unsigned long long int)[self longLongIntForColumnIndex:columnIdx];
}
- (BOOL)boolForColumn:(NSString*)columnName {
return [self boolForColumnIndex:[self columnIndexForName:columnName]];
}
- (BOOL)boolForColumnIndex:(int)columnIdx {
return ([self intForColumnIndex:columnIdx] != 0);
}
- (double)doubleForColumn:(NSString*)columnName {
return [self doubleForColumnIndex:[self columnIndexForName:columnName]];
}
- (double)doubleForColumnIndex:(int)columnIdx {
return sqlite3_column_double([_statement statement], columnIdx);
}
- (NSString*)stringForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
if (!c) {
// null row.
return nil;
}
return [NSString stringWithUTF8String:c];
}
- (NSString*)stringForColumn:(NSString*)columnName {
return [self stringForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSDate*)dateForColumn:(NSString*)columnName {
return [self dateForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSDate*)dateForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
return [_parentDB hasDateFormatter] ? [_parentDB dateFromString:[self stringForColumnIndex:columnIdx]] : [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]];
}
- (NSData*)dataForColumn:(NSString*)columnName {
return [self dataForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSData*)dataForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
if (dataBuffer == NULL) {
return nil;
}
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
}
- (NSData*)dataNoCopyForColumn:(NSString*)columnName {
return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
NSData *data = [NSData dataWithBytesNoCopy:(void *)dataBuffer length:(NSUInteger)dataSize freeWhenDone:NO];
return data;
}
- (BOOL)columnIndexIsNull:(int)columnIdx {
return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL;
}
- (BOOL)columnIsNull:(NSString*)columnName {
return [self columnIndexIsNull:[self columnIndexForName:columnName]];
}
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
return sqlite3_column_text([_statement statement], columnIdx);
}
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName {
return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]];
}
- (id)objectForColumnIndex:(int)columnIdx {
int columnType = sqlite3_column_type([_statement statement], columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [self dataForColumnIndex:columnIdx];
}
else {
//default to a string for everything else
returnValue = [self stringForColumnIndex:columnIdx];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
- (id)objectForColumnName:(NSString*)columnName {
return [self objectForColumnIndex:[self columnIndexForName:columnName]];
}
// returns autoreleased NSString containing the name of the column in the result set
- (NSString*)columnNameForIndex:(int)columnIdx {
return [NSString stringWithUTF8String: sqlite3_column_name([_statement statement], columnIdx)];
}
- (void)setParentDB:(FMDatabase *)newDb {
_parentDB = newDb;
}
- (id)objectAtIndexedSubscript:(int)columnIdx {
return [self objectForColumnIndex:columnIdx];
}
- (id)objectForKeyedSubscript:(NSString *)columnName {
return [self objectForColumnName:columnName];
}
// Brent 22 Feb. 2019: Calls to lowerCaseString show up in Instruments too much.
// Given that the amount of column names in a given app is going to be pretty small,
// we can just cache the lowercase versions
- (NSString *)_lowercaseString:(NSString *)s {
static NSLock *lock = nil;
static NSMutableDictionary *lowercaseStringCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
lowercaseStringCache = [[NSMutableDictionary alloc] init];
});
[lock lock];
NSString *lowercaseString = lowercaseStringCache[s];
if (lowercaseString == nil) {
lowercaseString = s.lowercaseString;
lowercaseStringCache[s] = lowercaseString;
}
[lock unlock];
return lowercaseString;
}
@end

View File

@ -0,0 +1,36 @@
//
// NSString+RSDatabase.h
// RSDatabase
//
// Created by Brent Simmons on 3/27/15.
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
//
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
@interface NSString (QSDatabase)
/*Returns @"(?, ?, ?)" -- where number of ? spots is specified by numberOfValues.
numberOfValues should be greater than 0. Triggers an NSParameterAssert if not.*/
+ (nullable NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues;
/*Returns @"(someColumn, anotherColumm, thirdColumn)" -- using passed-in keys.
It's essential that you trust keys. They must not be user input.
Triggers an NSParameterAssert if keys are empty.*/
+ (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys;
/*Returns @"key1=?, key2=?" using passed-in keys. Keys must be trusted.*/
+ (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,135 @@
//
// NSString+RSDatabase.m
// RSDatabase
//
// Created by Brent Simmons on 3/27/15.
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
//
#import "NSString+RSDatabase.h"
@implementation NSString (RSDatabase)
+ (NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues {
// @"(?, ?, ?)"
NSParameterAssert(numberOfValues > 0);
if (numberOfValues < 1) {
return nil;
}
static NSMutableDictionary *cache = nil;
static dispatch_once_t onceToken;
static NSLock *lock = nil;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
cache = [NSMutableDictionary new];
});
[lock lock];
NSNumber *cacheKey = @(numberOfValues);
NSString *cachedString = cache[cacheKey];
if (cachedString) {
[lock unlock];
return cachedString;
}
NSMutableString *s = [[NSMutableString alloc] initWithString:@"("];
NSUInteger i = 0;
for (i = 0; i < numberOfValues; i++) {
[s appendString:@"?"];
BOOL isLast = (i == (numberOfValues - 1));
if (!isLast) {
[s appendString:@", "];
}
}
[s appendString:@")"];
cache[cacheKey] = s;
[lock unlock];
return s;
}
+ (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys {
NSParameterAssert(keys.count > 0);
static NSMutableDictionary *cache = nil;
static dispatch_once_t onceToken;
static NSLock *lock = nil;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
cache = [NSMutableDictionary new];
});
[lock lock];
NSArray *cacheKey = keys;
NSString *cachedString = cache[cacheKey];
if (cachedString) {
[lock unlock];
return cachedString;
}
NSString *s = [NSString stringWithFormat:@"(%@)", [keys componentsJoinedByString:@", "]];
cache[cacheKey] = s;
[lock unlock];
return s;
}
+ (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys {
// key1=?, key2=?
NSParameterAssert(keys.count > 0);
static NSMutableDictionary *cache = nil;
static dispatch_once_t onceToken;
static NSLock *lock = nil;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
cache = [NSMutableDictionary new];
});
[lock lock];
NSArray *cacheKey = keys;
NSString *cachedString = cache[cacheKey];
if (cachedString) {
[lock unlock];
return cachedString;
}
NSMutableString *s = [NSMutableString stringWithString:@""];
NSUInteger i = 0;
NSUInteger numberOfKeys = [keys count];
for (i = 0; i < numberOfKeys; i++) {
NSString *oneKey = keys[i];
[s appendString:oneKey];
[s appendString:@"=?"];
BOOL isLast = (i == (numberOfKeys - 1));
if (!isLast) {
[s appendString:@", "];
}
}
cache[cacheKey] = s;
[lock unlock];
return s;
}
@end

View File

@ -0,0 +1,63 @@
//
// RSDatabaseQueue.h
// RSDatabase
//
// Created by Brent Simmons on 10/19/13.
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
//
@import Foundation;
#import "FMDatabase.h"
// This has been deprecated — use DatabaseQueue instead.
@class RSDatabaseQueue;
NS_ASSUME_NONNULL_BEGIN
@protocol RSDatabaseQueueDelegate <NSObject>
@optional
- (void)makeFunctionsForDatabase:(FMDatabase *)database queue:(RSDatabaseQueue *)queue;
@end
// Everything runs on a serial queue.
typedef void (^RSDatabaseBlock)(FMDatabase * __nonnull database);
@interface RSDatabaseQueue : NSObject
@property (nonatomic, strong, readonly) NSString *databasePath; // For debugging use, so you can open the database in sqlite3.
- (instancetype)initWithFilepath:(NSString *)filepath excludeFromBackup:(BOOL)excludeFromBackup;
@property (nonatomic, weak) id<RSDatabaseQueueDelegate> delegate;
// You can feed it the contents of a file that includes comments, etc.
// Lines that start with case-insensitive "create " are executed.
- (void)createTablesUsingStatements:(NSString *)createStatements;
- (void)createTablesUsingStatementsSync:(NSString *)createStatements;
- (void)update:(RSDatabaseBlock)updateBlock;
- (void)updateSync:(RSDatabaseBlock)updateBlock;
- (void)runInDatabase:(RSDatabaseBlock)databaseBlock; // Same as update, but no transaction.
- (void)fetch:(RSDatabaseBlock)fetchBlock;
- (void)fetchSync:(RSDatabaseBlock)fetchBlock;
- (void)vacuum;
- (void)vacuumIfNeeded; // defaultsKey = @"lastVacuumDate"; interval is 6 days.
- (void)vacuumIfNeeded:(NSString *)defaultsKey intervalBetweenVacuums:(NSTimeInterval)intervalBetweenVacuums;
- (NSArray *)arrayWithSingleColumnResultSet:(FMResultSet *)rs;
- (void)close;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,227 @@
//
// RSDatabaseQueue.m
// RSDatabase
//
// Created by Brent Simmons on 10/19/13.
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
//
#import "RSDatabaseQueue.h"
#import <sqlite3.h>
// This has been deprecated  use DatabaseQueue instead.
@interface RSDatabaseQueue ()
@property (nonatomic, strong, readwrite) NSString *databasePath;
@property (nonatomic, assign) BOOL excludeFromBackup;
@property (nonatomic, strong, readonly) dispatch_queue_t serialDispatchQueue;
@property (nonatomic) BOOL closing;
@property (nonatomic) BOOL closed;
@end
@implementation RSDatabaseQueue
#pragma mark - Init
- (instancetype)initWithFilepath:(NSString *)filepath excludeFromBackup:(BOOL)excludeFromBackup {
self = [super init];
if (self == nil)
return self;
_databasePath = filepath;
_serialDispatchQueue = dispatch_queue_create([[NSString stringWithFormat:@"RSDatabaseQueue serial queue - %@", filepath.lastPathComponent] UTF8String], DISPATCH_QUEUE_SERIAL);
_excludeFromBackup = excludeFromBackup;
return self;
}
#pragma mark - Database
- (FMDatabase *)database {
/*I've always done it this way -- kept a per-thread database in the threadDictionary -- and I know it's solid. Maybe it's not necessary with a serial queue, but my understanding was that SQLite wanted a different database per thread (and a serial queue may run on different threads).*/
if (self.closed) {
return nil;
}
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
FMDatabase *database = threadDictionary[self.databasePath];
if (!database || !database.open) {
database = [FMDatabase databaseWithPath:self.databasePath];
[database open];
[database executeUpdate:@"PRAGMA synchronous = 1;"];
[database setShouldCacheStatements:YES];
if ([self.delegate respondsToSelector:@selector(makeFunctionsForDatabase:queue:)]) {
[self.delegate makeFunctionsForDatabase:database queue:self];
}
threadDictionary[self.databasePath] = database;
if (self.excludeFromBackup) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURL *URL = [NSURL fileURLWithPath:self.databasePath isDirectory:NO];
NSError *error = nil;
[URL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error];
});
}
}
return database;
}
#pragma mark - API
- (void)createTablesUsingStatements:(NSString *)createStatements {
[self runInDatabase:^(FMDatabase *database) {
[self runCreateStatements:createStatements database:database];
}];
}
- (void)createTablesUsingStatementsSync:(NSString *)createStatements {
[self runInDatabaseSync:^(FMDatabase *database) {
[self runCreateStatements:createStatements database:database];
}];
}
- (void)runCreateStatements:(NSString *)createStatements database:(FMDatabase *)database {
[createStatements enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
if ([line.lowercaseString hasPrefix:@"create "]) {
[database executeUpdate:line];
}
*stop = NO;
}];
}
- (void)update:(RSDatabaseBlock)updateBlock {
dispatch_async(self.serialDispatchQueue, ^{
[self runInTransaction:updateBlock];
});
}
- (void)updateSync:(RSDatabaseBlock)updateBlock {
dispatch_sync(self.serialDispatchQueue, ^{
[self runInTransaction:updateBlock];
});
}
- (void)runInTransaction:(RSDatabaseBlock)databaseBlock {
@autoreleasepool {
FMDatabase *database = [self database];
[database beginTransaction];
databaseBlock(database);
[database commit];
}
}
- (void)runInDatabase:(RSDatabaseBlock)databaseBlock {
dispatch_async(self.serialDispatchQueue, ^{
@autoreleasepool {
databaseBlock([self database]);
}
});
}
- (void)runInDatabaseSync:(RSDatabaseBlock)databaseBlock {
dispatch_sync(self.serialDispatchQueue, ^{
@autoreleasepool {
databaseBlock([self database]);
}
});
}
- (void)fetch:(RSDatabaseBlock)fetchBlock {
[self runInDatabase:fetchBlock];
}
- (void)fetchSync:(RSDatabaseBlock)fetchBlock {
dispatch_sync(self.serialDispatchQueue, ^{
@autoreleasepool {
fetchBlock([self database]);
}
});
}
- (void)vacuum {
dispatch_async(self.serialDispatchQueue, ^{
@autoreleasepool {
[[self database] executeUpdate:@"vacuum;"];
}
});
}
- (void)vacuumIfNeeded {
NSTimeInterval interval = (24 * 60 * 60) * 6; // 6 days
[self vacuumIfNeeded:@"lastVacuumDate" intervalBetweenVacuums:interval];
}
- (void)vacuumIfNeeded:(NSString *)defaultsKey intervalBetweenVacuums:(NSTimeInterval)intervalBetweenVacuums {
NSDate *lastVacuumDate = [[NSUserDefaults standardUserDefaults] objectForKey:defaultsKey];
if (!lastVacuumDate || ![lastVacuumDate isKindOfClass:[NSDate class]]) {
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:defaultsKey];
return;
}
NSDate *cutoffDate = [[NSDate date] dateByAddingTimeInterval: -(intervalBetweenVacuums)];
if ([cutoffDate earlierDate:lastVacuumDate] == lastVacuumDate) {
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:defaultsKey];
[self vacuum];
}
}
- (NSArray *)arrayWithSingleColumnResultSet:(FMResultSet *)rs {
NSMutableArray *results = [NSMutableArray new];
while ([rs next]) {
id oneObject = [rs objectForColumnIndex:0];
if (oneObject) {
[results addObject:oneObject];
}
}
return [results copy];
}
- (void)close {
self.closing = YES;
[self runInDatabaseSync:^(FMDatabase *database) {
self.closed = YES;
[database close];
}];
}
@end

View File

@ -0,0 +1,17 @@
// RSDatabaseObjC
// FMDB
#import "../FMDatabase.h"
#import "../FMDatabaseAdditions.h"
#import "../FMResultSet.h"
// Categories
#import "../FMDatabase+RSExtras.h"
#import "../FMResultSet+RSExtras.h"
#import "../NSString+RSDatabase.h"
// RSDatabase
#import "../RSDatabaseQueue.h"

View File

@ -0,0 +1,156 @@
//
// ODBTests.swift
// RSDatabaseTests
//
// Created by Brent Simmons on 8/27/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import XCTest
import RSDatabase
class ODBTests: XCTestCase {
func testODBCreation() {
let odb = genericTestODB()
closeAndDelete(odb)
}
func testSimpleBoolStorage() {
let odb = genericTestODB()
let path = ODBPath.path(["testBool"])
path.setRawValue(true, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! Bool, true)
closeAndDelete(odb)
}
func testSimpleIntStorage() {
let odb = genericTestODB()
let path = ODBPath.path(["TestInt"])
let intValue = 3487456
path.setRawValue(intValue, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! Int, intValue)
closeAndDelete(odb)
}
func testSimpleDoubleStorage() {
let odb = genericTestODB()
let path = ODBPath.path(["TestDouble"])
let doubleValue = 3498.45745
path.setRawValue(doubleValue, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! Double, doubleValue)
closeAndDelete(odb)
}
func testReadSimpleBoolPerformance() {
let odb = genericTestODB()
let path = ODBPath.path(["TestBool"])
path.setRawValue(true, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! Bool, true)
self.measure {
let _ = path.rawValue(with: odb)
}
closeAndDelete(odb)
}
func testSetSimpleUnchangingBoolPerformance() {
let odb = genericTestODB()
let path = ODBPath.path(["TestBool"])
self.measure {
path.setRawValue(true, odb: odb)
}
closeAndDelete(odb)
}
func testReadAndCloseAndReadSimpleBool() {
let f = pathForTestFile("testReadAndCloseAndReadSimpleBool.odb")
var odb = ODB(filepath: f)
let path = ODBPath.path(["testBool"])
path.setRawValue(true, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! Bool, true)
odb.close()
odb = ODB(filepath: f)
XCTAssertEqual(path.rawValue(with: odb) as! Bool, true)
closeAndDelete(odb)
}
func testReplaceSimpleObject() {
let odb = genericTestODB()
let path = ODBPath.path(["TestValue"])
let intValue = 3487456
path.setRawValue(intValue, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! Int, intValue)
let stringValue = "test string value"
path.setRawValue(stringValue, odb: odb)
XCTAssertEqual(path.rawValue(with: odb) as! String, stringValue)
closeAndDelete(odb)
}
func testEnsureTable() {
let odb = genericTestODB()
let path = ODBPath.path(["A", "B", "C", "D"])
let _ = path.ensureTable(with: odb)
closeAndDelete(odb)
}
func testEnsureTablePerformance() {
let odb = genericTestODB()
let path = ODBPath.path(["A", "B", "C", "D"])
self.measure {
let _ = path.ensureTable(with: odb)
}
closeAndDelete(odb)
}
func testStoreDateInSubtable() {
let odb = genericTestODB()
let path = ODBPath.path(["A", "B", "C", "D"])
path.ensureTable(with: odb)
let d = Date()
let datePath = path + "TestValue"
datePath.setRawValue(d, odb: odb)
XCTAssertEqual(datePath.rawValue(with: odb) as! Date, d)
closeAndDelete(odb)
}
}
private extension ODBTests {
func tempFolderPath() -> String {
return FileManager.default.temporaryDirectory.path
}
func pathForTestFile(_ name: String) -> String {
let folder = tempFolderPath()
return (folder as NSString).appendingPathComponent(name)
}
static var databaseFileID = 0;
func pathForGenericTestFile() -> String {
ODBTests.databaseFileID += 1
return pathForTestFile("Test\(ODBTests.databaseFileID).odb")
}
func genericTestODB() -> ODB {
let f = pathForGenericTestFile()
return ODB(filepath: f)
}
func closeAndDelete(_ odb: ODB) {
odb.close()
try! FileManager.default.removeItem(atPath: odb.filepath)
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

21
RSTree/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Brent Simmons
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
RSTree/Package.swift Normal file
View File

@ -0,0 +1,19 @@
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "RSTree",
products: [
.library(
name: "RSTree",
type: .dynamic,
targets: ["RSTree"]),
],
targets: [
.target(
name: "RSTree",
dependencies: [],
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
)
]
)

3
RSTree/README.md Normal file
View File

@ -0,0 +1,3 @@
# RSTree
Tree things. Used by Evergreen. Probably will get used by Frontier.

View File

@ -0,0 +1,58 @@
//
// NSOutlineView+RSTree.swift
// RSTree
//
// Created by Brent Simmons on 9/5/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(OSX)
import AppKit
public extension NSOutlineView {
@discardableResult
func revealAndSelectNodeAtPath(_ nodePath: NodePath) -> Bool {
// Returns true on success. Expands folders on the way. May succeed partially (returns false, in that case).
let numberOfNodes = nodePath.components.count
if numberOfNodes < 2 {
return false
}
let indexOfNodeToSelect = numberOfNodes - 1
for i in 1...indexOfNodeToSelect { // Start at 1 to skip root node.
let oneNode = nodePath.components[i]
let oneRow = row(forItem: oneNode)
if oneRow < 0 {
return false
}
if i == indexOfNodeToSelect {
selectRowIndexes(NSIndexSet(index: oneRow) as IndexSet, byExtendingSelection: false)
scrollRowToVisible(oneRow)
return true
}
else {
expandItem(oneNode)
}
}
return false
}
@discardableResult
func revealAndSelectRepresentedObject(_ representedObject: AnyObject, _ treeController: TreeController) -> Bool {
guard let nodePath = NodePath(representedObject: representedObject, treeController: treeController) else {
return false
}
return revealAndSelectNodeAtPath(nodePath)
}
}
#endif

View File

@ -0,0 +1,224 @@
//
// Node.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/21/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Main thread only.
public final class Node: Hashable {
public weak var parent: Node?
public let representedObject: AnyObject
public var canHaveChildNodes = false
public var isGroupItem = false
public var childNodes = [Node]()
public let uniqueID: Int
private static var incrementingID = 0
public var isRoot: Bool {
if let _ = parent {
return false
}
return true
}
public var numberOfChildNodes: Int {
return childNodes.count
}
public var indexPath: IndexPath {
if let parent = parent {
let parentPath = parent.indexPath
if let childIndex = parent.indexOfChild(self) {
return parentPath.appending(childIndex)
}
preconditionFailure("A Nodes parent must contain it as a child.")
}
return IndexPath(index: 0) //root node
}
public var level: Int {
if let parent = parent {
return parent.level + 1
}
return 0
}
public var isLeaf: Bool {
return numberOfChildNodes < 1
}
public init(representedObject: AnyObject, parent: Node?) {
precondition(Thread.isMainThread)
self.representedObject = representedObject
self.parent = parent
self.uniqueID = Node.incrementingID
Node.incrementingID += 1
}
public class func genericRootNode() -> Node {
let node = Node(representedObject: TopLevelRepresentedObject(), parent: nil)
node.canHaveChildNodes = true
return node
}
public func existingOrNewChildNode(with representedObject: AnyObject) -> Node {
if let node = childNodeRepresentingObject(representedObject) {
return node
}
return createChildNode(representedObject)
}
public func createChildNode(_ representedObject: AnyObject) -> Node {
// Just creates doesnt add it.
return Node(representedObject: representedObject, parent: self)
}
public func childAtIndex(_ index: Int) -> Node? {
if index >= childNodes.count || index < 0 {
return nil
}
return childNodes[index]
}
public func indexOfChild(_ node: Node) -> Int? {
return childNodes.firstIndex{ (oneChildNode) -> Bool in
oneChildNode === node
}
}
public func childNodeRepresentingObject(_ obj: AnyObject) -> Node? {
return findNodeRepresentingObject(obj, recursively: false)
}
public func descendantNodeRepresentingObject(_ obj: AnyObject) -> Node? {
return findNodeRepresentingObject(obj, recursively: true)
}
public func descendantNode(where test: (Node) -> Bool) -> Node? {
return findNode(where: test, recursively: true)
}
public func hasAncestor(in nodes: [Node]) -> Bool {
for node in nodes {
if node.isAncestor(of: self) {
return true
}
}
return false
}
public func isAncestor(of node: Node) -> Bool {
if node == self {
return false
}
var nomad = node
while true {
guard let parent = nomad.parent else {
return false
}
if parent == self {
return true
}
nomad = parent
}
}
public class func nodesOrganizedByParent(_ nodes: [Node]) -> [Node: [Node]] {
let nodesWithParents = nodes.filter { $0.parent != nil }
return Dictionary(grouping: nodesWithParents, by: { $0.parent! })
}
public class func indexSetsGroupedByParent(_ nodes: [Node]) -> [Node: IndexSet] {
let d = nodesOrganizedByParent(nodes)
let indexSetDictionary = d.mapValues { (nodes) -> IndexSet in
var indexSet = IndexSet()
if nodes.isEmpty {
return indexSet
}
let parent = nodes.first!.parent!
for node in nodes {
if let index = parent.indexOfChild(node) {
indexSet.insert(index)
}
}
return indexSet
}
return indexSetDictionary
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(uniqueID)
}
// MARK: - Equatable
public class func ==(lhs: Node, rhs: Node) -> Bool {
return lhs === rhs
}
}
public extension Array where Element == Node {
func representedObjects() -> [AnyObject] {
return self.map{ $0.representedObject }
}
}
private extension Node {
func findNodeRepresentingObject(_ obj: AnyObject, recursively: Bool = false) -> Node? {
for childNode in childNodes {
if childNode.representedObject === obj {
return childNode
}
if recursively, let foundNode = childNode.descendantNodeRepresentingObject(obj) {
return foundNode
}
}
return nil
}
func findNode(where test: (Node) -> Bool, recursively: Bool = false) -> Node? {
for childNode in childNodes {
if test(childNode) {
return childNode
}
if recursively, let foundNode = childNode.findNode(where: test, recursively: recursively) {
return foundNode
}
}
return nil
}
}

View File

@ -0,0 +1,42 @@
//
// NodePath.swift
// RSTree
//
// Created by Brent Simmons on 9/5/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct NodePath {
let components: [Node]
public init(node: Node) {
var tempArray = [node]
var nomad: Node = node
while true {
if let parent = nomad.parent {
tempArray.append(parent)
nomad = parent
}
else {
break
}
}
self.components = tempArray.reversed()
}
public init?(representedObject: AnyObject, treeController: TreeController) {
if let node = treeController.nodeInTreeRepresentingObject(representedObject) {
self.init(node: node)
}
else {
return nil
}
}
}

View File

@ -0,0 +1,3 @@
struct RSTree {
var text = "Hello, World!"
}

View File

@ -0,0 +1,15 @@
//
// TopLevelRepresentedObject.swift
// RSTree
//
// Created by Brent Simmons on 8/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Handy to use as the represented object for a root node. Not required to use it, though.
final class TopLevelRepresentedObject {
}

View File

@ -0,0 +1,135 @@
//
// TreeController.swift
// NetNewsWire
//
// Created by Brent Simmons on 5/29/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public protocol TreeControllerDelegate: AnyObject {
func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]?
}
public typealias NodeVisitBlock = (_ : Node) -> Void
public final class TreeController {
private weak var delegate: TreeControllerDelegate?
public let rootNode: Node
public init(delegate: TreeControllerDelegate, rootNode: Node) {
self.delegate = delegate
self.rootNode = rootNode
rebuild()
}
public convenience init(delegate: TreeControllerDelegate) {
self.init(delegate: delegate, rootNode: Node.genericRootNode())
}
@discardableResult
public func rebuild() -> Bool {
// Rebuild and re-sort. Return true if any changes in the entire tree.
return rebuildChildNodes(node: rootNode)
}
public func visitNodes(_ visitBlock: NodeVisitBlock) {
visitNode(rootNode, visitBlock)
}
public func nodeInArrayRepresentingObject(nodes: [Node], representedObject: AnyObject, recurse: Bool = false) -> Node? {
for oneNode in nodes {
if oneNode.representedObject === representedObject {
return oneNode
}
if recurse, oneNode.canHaveChildNodes {
if let foundNode = nodeInArrayRepresentingObject(nodes: oneNode.childNodes, representedObject: representedObject, recurse: recurse) {
return foundNode
}
}
}
return nil
}
public func nodeInTreeRepresentingObject(_ representedObject: AnyObject) -> Node? {
return nodeInArrayRepresentingObject(nodes: [rootNode], representedObject: representedObject, recurse: true)
}
public func normalizedSelectedNodes(_ nodes: [Node]) -> [Node] {
// An array of nodes might include a leaf node and its parent. Remove the leaf node.
var normalizedNodes = [Node]()
for node in nodes {
if !node.hasAncestor(in: nodes) {
normalizedNodes += [node]
}
}
return normalizedNodes
}
}
private extension TreeController {
func visitNode(_ node: Node, _ visitBlock: NodeVisitBlock) {
visitBlock(node)
node.childNodes.forEach{ (oneChildNode) in
visitNode(oneChildNode, visitBlock)
}
}
func nodeArraysAreEqual(_ nodeArray1: [Node]?, _ nodeArray2: [Node]?) -> Bool {
if nodeArray1 == nil && nodeArray2 == nil {
return true
}
if nodeArray1 != nil && nodeArray2 == nil {
return false
}
if nodeArray1 == nil && nodeArray2 != nil {
return false
}
return nodeArray1! == nodeArray2!
}
func rebuildChildNodes(node: Node) -> Bool {
if !node.canHaveChildNodes {
return false
}
var childNodesDidChange = false
let childNodes = delegate?.treeController(treeController: self, childNodesFor: node) ?? [Node]()
childNodesDidChange = !nodeArraysAreEqual(childNodes, node.childNodes)
if (childNodesDidChange) {
node.childNodes = childNodes
}
childNodes.forEach{ (oneChildNode) in
if rebuildChildNodes(node: oneChildNode) {
childNodesDidChange = true
}
}
return childNodesDidChange
}
}

View File

@ -10,16 +10,19 @@ let package = Package(
name: "RSWeb",
type: .dynamic,
targets: ["RSWeb"]),
],
dependencies: [
],
targets: [
.target(
name: "RSWeb",
],
dependencies: [
],
targets: [
.target(
name: "RSWeb",
resources: [.copy("UTS46/uts46")],
swiftSettings: [.define("SWIFT_PACKAGE")]),
.testTarget(
name: "RSWebTests",
dependencies: ["RSWeb"]),
]
swiftSettings: [
.define("SWIFT_PACKAGE"),
.unsafeFlags(["-warnings-as-errors"])
]),
.testTarget(
name: "RSWebTests",
dependencies: ["RSWeb"]),
]
)