Make RSTree and RSDatabase local packages.
This commit is contained in:
parent
fe728c72b3
commit
7e2c668974
@ -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 */;
|
||||
|
@ -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",
|
||||
|
@ -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
18
RSDatabase/.github/workflows/build.yml
vendored
Normal 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
61
RSDatabase/.gitignore
vendored
Normal 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
21
RSDatabase/LICENSE
Executable 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
35
RSDatabase/Package.swift
Normal 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
12
RSDatabase/README.md
Executable file
@ -0,0 +1,12 @@
|
||||
# RSDatabase
|
||||
This is utility code for using SQLite via FMDB. It’s not a persistence framework — it’s lower-level.
|
||||
|
||||
It builds as a couple frameworks — one for Mac, one for iOS.
|
||||
|
||||
It has no additional dependencies, but that’s because FMDB is actually included — you might want to instead make sure you have the [latest FMDB](https://github.com/ccgus/fmdb), which isn’t 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 don’t have to write as much SQL.
|
53
RSDatabase/Sources/RSDatabase/Database.swift
Normal file
53
RSDatabase/Sources/RSDatabase/Database.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
61
RSDatabase/Sources/RSDatabase/DatabaseObject.swift
Normal file
61
RSDatabase/Sources/RSDatabase/DatabaseObject.swift
Normal 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
|
||||
}
|
||||
}
|
33
RSDatabase/Sources/RSDatabase/DatabaseObjectCache.swift
Normal file
33
RSDatabase/Sources/RSDatabase/DatabaseObjectCache.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
259
RSDatabase/Sources/RSDatabase/DatabaseQueue.swift
Normal file
259
RSDatabase/Sources/RSDatabase/DatabaseQueue.swift
Normal 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 it’s 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 don’t 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 isn’t 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, it’s 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 it’s 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 it’s 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)) // Doesn’t 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)
|
||||
}
|
||||
}
|
||||
|
139
RSDatabase/Sources/RSDatabase/DatabaseTable.swift
Normal file
139
RSDatabase/Sources/RSDatabase/DatabaseTable.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
|
179
RSDatabase/Sources/RSDatabase/ODB/ODB.swift
Normal file
179
RSDatabase/Sources/RSDatabase/ODB/ODB.swift
Normal 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.
|
||||
// It’s 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.
|
||||
/// It’s 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 there’s 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;
|
||||
"""
|
||||
}
|
||||
|
||||
|
18
RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift
Normal file
18
RSDatabase/Sources/RSDatabase/ODB/ODBObject.swift
Normal 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 }
|
||||
}
|
196
RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift
Normal file
196
RSDatabase/Sources/RSDatabase/ODB/ODBPath.swift
Normal 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, it’s ignored: "root" is implied.
|
||||
An empty array or ["root"] refers to the root table.
|
||||
A path does not necessarily point to something that exists. It’s 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.
|
||||
/// Won’t 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: don’t 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
|
||||
}
|
||||
}
|
42
RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift
Normal file
42
RSDatabase/Sources/RSDatabase/ODB/ODBRawValueTable.swift
Normal 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 you’re 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
|
||||
}
|
||||
}
|
170
RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift
Normal file
170
RSDatabase/Sources/RSDatabase/ODB/ODBTable.swift
Normal 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 {
|
||||
// Don’t 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
|
||||
}
|
||||
}
|
56
RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift
Normal file
56
RSDatabase/Sources/RSDatabase/ODB/ODBTablesTable.swift
Normal 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)
|
||||
}
|
||||
}
|
164
RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift
Normal file
164
RSDatabase/Sources/RSDatabase/ODB/ODBValue.swift
Normal 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: they’re 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
|
||||
}
|
||||
}
|
||||
}
|
40
RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift
Normal file
40
RSDatabase/Sources/RSDatabase/ODB/ODBValueObject.swift
Normal 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
|
||||
}
|
||||
}
|
97
RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift
Normal file
97
RSDatabase/Sources/RSDatabase/ODB/ODBValuesTable.swift
Normal 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)
|
||||
}
|
||||
}
|
149
RSDatabase/Sources/RSDatabase/ODB/README.markdown
Normal file
149
RSDatabase/Sources/RSDatabase/ODB/README.markdown
Normal file
@ -0,0 +1,149 @@
|
||||
# ODB
|
||||
|
||||
**NOTE**: This all has been excluded from building. It’s a work in progress, not ready for use.
|
||||
|
||||
ODB stands for Object Database.
|
||||
|
||||
“Object” doesn’t mean object in the object-oriented programming sense — it just means *thing*.
|
||||
|
||||
Think of the ODB as a nested Dictionary that’s *persistent*. It’s schema-less. Tables (which are like dictionaries) can contain other tables. It’s 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.
|
||||
|
||||
It’s 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 it’s 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 Frontier’s 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 Winer’s.)
|
||||
|
||||
## How to use it
|
||||
|
||||
### Create an ODB
|
||||
|
||||
`let odb = ODB(filepath: somePath)` creates a new ODB for that path. If there’s an existing database on disk, it uses that one. Otherwise it creates a new one.
|
||||
|
||||
### Ensuring that a table exists
|
||||
|
||||
Let’s say you’re writing an RSS reader, and you want to make sure there’s 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 it’s 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 don’t exist.
|
||||
|
||||
Operations referencing `ODBTable` and `ODBValueObject` must be enclosed in an `ODB.perform` block. This is for thread safety. If you don’t 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`: it’s a protocol.
|
||||
|
||||
### Setting a value
|
||||
|
||||
Let’s 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 you’ve 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 doesn’t exist, then it gets created.
|
||||
|
||||
(Yes, this would be so much easier in a scripting language. You’d 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, it’s good practice to use that ability sparingly. When you can break things down into simple primitive types, that’s best. Treating an entire table, with multiple stored values, as a unit is often the way to go. But not always.
|
||||
|
||||
### Getting a value
|
||||
|
||||
Let’s say you want to get back the edited name of the feed. You’d 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. It’s an immutable struct with no connection to the database. But in reality you’d probably write the above code more like this:
|
||||
|
||||
var name: String?
|
||||
ODB.perform {
|
||||
name = path.value? as? String
|
||||
}
|
||||
|
||||
It’s totally a-okay to use Swift’s built-in types this way instead of checking the ODBValue’s `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 feed’s name, and now you want to delete `RSS.feeds.[feedID].editedName` — given the path, you’d 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 won’t use this directly all that often. It wraps an `ODBValue`, which you’ll use way more often. The useful API for `ODBValueObject` is almost entirely in `ODBObject`.
|
||||
|
||||
## Notes
|
||||
|
||||
### The root table
|
||||
|
||||
The one table you can’t delete is the root table — every ODB has a top-level table named `root`. You don’t usually specify `root` as the first part of a path, but you could. It’s implied.
|
||||
|
||||
A path like `["RSS", "feeds"]` is precisely the same as `["root", "RSS", "feeds"]` — they’re interchangeable paths.
|
||||
|
||||
### Case-sensitivity
|
||||
|
||||
Frontier’s object database was case-insensitive: you could refer to the "feeds" table as the "FEeDs" table — it would be the same thing.
|
||||
|
||||
While I don’t know this for sure, I assume this was because the Mac’s file system is also case-insensitive. This was considered one of the user-friendly things about Macs.
|
||||
|
||||
We’re 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 machine’s locale actually is. This is something to be aware of.
|
||||
|
||||
### Caching and Performance
|
||||
|
||||
The database is cached in memory as it is used. A table’s children are not read into memory until referenced.
|
||||
|
||||
For objects already in memory, reads are fast since there’s no need to query the SQLite database.
|
||||
|
||||
If this caching becomes a problem in production use — if it tends to use too much memory — we’ll 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 it’s 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 don’t exist. The database can change while you hold an `ODBPath` reference, and that’s okay: it’s 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 doesn’t 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 app’s 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, it’s necessary to use a lock when working with these — that’s 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.
|
||||
|
||||
|
||||
|
@ -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] {
|
||||
|
||||
// Can’t create a Set, because we can’t make a Set<DatabaseObject>, because protocol-conforming objects can’t 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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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).
|
||||
// It’s 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]
|
||||
}
|
||||
}
|
83
RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.h
Executable file
83
RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.h
Executable 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
|
180
RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.m
Executable file
180
RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.m
Executable 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
|
||||
|
1084
RSDatabase/Sources/RSDatabaseObjC/FMDatabase.h
Executable file
1084
RSDatabase/Sources/RSDatabaseObjC/FMDatabase.h
Executable file
File diff suppressed because it is too large
Load Diff
1427
RSDatabase/Sources/RSDatabaseObjC/FMDatabase.m
Executable file
1427
RSDatabase/Sources/RSDatabaseObjC/FMDatabase.m
Executable file
File diff suppressed because it is too large
Load Diff
281
RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.h
Executable file
281
RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.h
Executable 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
|
225
RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.m
Executable file
225
RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.m
Executable 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
|
23
RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.h
Executable file
23
RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.h
Executable 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
|
51
RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.m
Executable file
51
RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.m
Executable 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
|
469
RSDatabase/Sources/RSDatabaseObjC/FMResultSet.h
Executable file
469
RSDatabase/Sources/RSDatabaseObjC/FMResultSet.h
Executable 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
|
||||
|
454
RSDatabase/Sources/RSDatabaseObjC/FMResultSet.m
Executable file
454
RSDatabase/Sources/RSDatabaseObjC/FMResultSet.m
Executable 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
|
36
RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.h
Executable file
36
RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.h
Executable 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
|
135
RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.m
Executable file
135
RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.m
Executable 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
|
63
RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.h
Executable file
63
RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.h
Executable 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
|
227
RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.m
Executable file
227
RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.m
Executable 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
|
||||
|
17
RSDatabase/Sources/RSDatabaseObjC/include/RSDatabaseObjC.h
Normal file
17
RSDatabase/Sources/RSDatabaseObjC/include/RSDatabaseObjC.h
Normal 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"
|
156
RSDatabase/Tests/RSDatabaseTests/ODBTests.swift
Normal file
156
RSDatabase/Tests/RSDatabaseTests/ODBTests.swift
Normal 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)
|
||||
}
|
||||
}
|
7
RSTree/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
RSTree/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
generated
Normal 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
21
RSTree/LICENSE
Normal 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
19
RSTree/Package.swift
Normal 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
3
RSTree/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# RSTree
|
||||
|
||||
Tree things. Used by Evergreen. Probably will get used by Frontier.
|
58
RSTree/Sources/RSTree/NSOutlineView+RSTree.swift
Normal file
58
RSTree/Sources/RSTree/NSOutlineView+RSTree.swift
Normal 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
|
224
RSTree/Sources/RSTree/Node.swift
Normal file
224
RSTree/Sources/RSTree/Node.swift
Normal 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 Node’s 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 — doesn’t 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
|
||||
}
|
||||
|
||||
}
|
42
RSTree/Sources/RSTree/NodePath.swift
Normal file
42
RSTree/Sources/RSTree/NodePath.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
3
RSTree/Sources/RSTree/RSTree.swift
Normal file
3
RSTree/Sources/RSTree/RSTree.swift
Normal file
@ -0,0 +1,3 @@
|
||||
struct RSTree {
|
||||
var text = "Hello, World!"
|
||||
}
|
15
RSTree/Sources/RSTree/TopLevelRepresentedObject.swift
Normal file
15
RSTree/Sources/RSTree/TopLevelRepresentedObject.swift
Normal 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 {
|
||||
|
||||
}
|
135
RSTree/Sources/RSTree/TreeController.swift
Normal file
135
RSTree/Sources/RSTree/TreeController.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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"]),
|
||||
]
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user