diff --git a/Modules/Sources/Storage/GRDB/GRDBManager.swift b/Modules/Sources/Storage/GRDB/GRDBManager.swift index 95029322a0a..bbe9eea76c7 100644 --- a/Modules/Sources/Storage/GRDB/GRDBManager.swift +++ b/Modules/Sources/Storage/GRDB/GRDBManager.swift @@ -1,35 +1,34 @@ import Foundation import GRDB -// TODO: remove ignore when we start using this -// periphery: ignore -public final class GRDBManager { +public protocol GRDBManagerProtocol { + var databaseConnection: GRDBDatabaseConnection { get } +} - let databaseQueue: DatabaseQueue - private let databasePath: String +public protocol GRDBDatabaseConnection: DatabaseReader & DatabaseWriter {} - public init(databasePath: String) throws { - self.databasePath = databasePath +public final class GRDBManager: GRDBManagerProtocol { + + public let databaseConnection: GRDBDatabaseConnection + public init(databasePath: String) throws { let databaseURL = URL(fileURLWithPath: databasePath) let directoryURL = databaseURL.deletingLastPathComponent() try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) - self.databaseQueue = try DatabaseQueue(path: databasePath) + self.databaseConnection = try DatabaseQueue(path: databasePath) try migrateIfNeeded() } + // Creates an in-memory database, intended for use in tests. init() throws { - self.databasePath = "in-memory" - self.databaseQueue = try DatabaseQueue() + self.databaseConnection = try DatabaseQueue() try migrateIfNeeded() } } -// TODO: remove ignore when we start using this -// periphery: ignore private extension GRDBManager { func migrateIfNeeded() throws { var migrator = DatabaseMigrator() @@ -43,6 +42,8 @@ private extension GRDBManager { try V001InitialSchema.migrate(db) } - try migrator.migrate(databaseQueue) + try migrator.migrate(databaseConnection) } } + +extension DatabaseQueue: GRDBDatabaseConnection {} diff --git a/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift b/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift index 51d4a998c3c..74a0624f4f6 100644 --- a/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift +++ b/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift @@ -1,8 +1,6 @@ import Foundation import GRDB -// TODO: remove ignore when we start using this -// periphery: ignore struct V001InitialSchema { // This migration is under development and not released yet. // It's still open for modification, until we ship. diff --git a/Modules/Tests/StorageTests/GRDB/GRDBManagerTests.swift b/Modules/Tests/StorageTests/GRDB/GRDBManagerTests.swift index 5f616899e27..626a195c958 100644 --- a/Modules/Tests/StorageTests/GRDB/GRDBManagerTests.swift +++ b/Modules/Tests/StorageTests/GRDB/GRDBManagerTests.swift @@ -18,7 +18,7 @@ struct GRDBManagerTests { let manager = try GRDBManager() // When - let tableExists = try manager.databaseQueue.read { db in + let tableExists = try manager.databaseConnection.read { db in return ( try db.tableExists("site"), try db.tableExists("product"), @@ -49,7 +49,7 @@ struct GRDBManagerTests { init() throws { self.manager = try GRDBManager() - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let record = TestSite(id: sampleSiteID) try record.insert(db) } @@ -60,7 +60,7 @@ struct GRDBManagerTests { // Given // When - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let record = TestProduct( siteID: 1, id: 100, @@ -74,7 +74,7 @@ struct GRDBManagerTests { } // Then - let productCount = try manager.databaseQueue.read { db in + let productCount = try manager.databaseConnection.read { db in try TestProduct.fetchCount(db) } @@ -84,7 +84,7 @@ struct GRDBManagerTests { @Test("Insert product variation with a relationship to a product") func test_after_init_can_insert_productVariation_with_foreign_key() throws { // Given – parent product - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let product = TestProduct( siteID: 1, id: 100, @@ -98,7 +98,7 @@ struct GRDBManagerTests { } // When - Insert variation - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let variation = TestProductVariation( siteID: 1, id: 200, @@ -110,7 +110,7 @@ struct GRDBManagerTests { } // Then - let variations = try manager.databaseQueue.read { db in + let variations = try manager.databaseConnection.read { db in try TestProductVariation.fetchAll(db) } @@ -121,7 +121,7 @@ struct GRDBManagerTests { @Test("Fetch variations by product ID") func test_after_init_and_insert_can_query_productVariation_using_foreign_key() throws { // Given parent product and some variations - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in // Insert product let product = TestProduct( siteID: 1, @@ -148,7 +148,7 @@ struct GRDBManagerTests { } // When - let variations = try manager.databaseQueue.read { db in + let variations = try manager.databaseConnection.read { db in try TestProductVariation .filter(Column("productID") == 100) .fetchAll(db) @@ -162,7 +162,7 @@ struct GRDBManagerTests { @Test("Insert product attribute with options array (JSON)") func test_after_init_can_insert_productAttribute_with_options_as_JSON_array() throws { // Given parent product - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in // Insert product first let product = TestProduct( siteID: 1, @@ -177,7 +177,7 @@ struct GRDBManagerTests { } // When - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let attribute = TestProductAttribute( productID: 100, name: "Color", @@ -190,7 +190,7 @@ struct GRDBManagerTests { } // Then - let attribute = try manager.databaseQueue.read { db in + let attribute = try manager.databaseConnection.read { db in try TestProductAttribute.fetchOne(db) } @@ -201,7 +201,7 @@ struct GRDBManagerTests { @Test("Insert variation attributes") func test_after_init_can_insert_variation_attributes() throws { // Given parent product and variation - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in // Insert product let product = TestProduct( siteID: 1, @@ -226,7 +226,7 @@ struct GRDBManagerTests { } // When - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let variationAttribute = TestProductVariationAttribute( productVariationID: 200, name: "Color", @@ -236,7 +236,7 @@ struct GRDBManagerTests { } // Then - let attributes = try manager.databaseQueue.read { db in + let attributes = try manager.databaseConnection.read { db in try TestProductVariationAttribute.fetchAll(db) } @@ -247,14 +247,14 @@ struct GRDBManagerTests { @Test("Deleting site cascades to all related entities") func test_deleting_site_cascades_to_all_related_entities() throws { // Given - Create a separate site for this test to avoid interfering with other tests - let testSiteId = try manager.databaseQueue.write { db -> Int64 in + let testSiteId = try manager.databaseConnection.write { db -> Int64 in let site = TestSite(id: 999) try site.insert(db) return 999 } // Insert full entity hierarchy - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let product = TestProduct( siteID: testSiteId, id: 100, @@ -294,7 +294,7 @@ struct GRDBManagerTests { } // Verify entities exist for test site - let countsBefore = try manager.databaseQueue.read { db in + let countsBefore = try manager.databaseConnection.read { db in return ( products: try TestProduct.filter(Column("siteID") == testSiteId).fetchCount(db), variations: try TestProductVariation.filter(Column("siteID") == testSiteId).fetchCount(db), @@ -309,12 +309,12 @@ struct GRDBManagerTests { #expect(countsBefore.variationAttributes == 1) // When - Delete the site - _ = try manager.databaseQueue.write { db in + _ = try manager.databaseConnection.write { db in try TestSite.filter(Column("id") == testSiteId).deleteAll(db) } // Then - All related entities should be deleted - let countsAfter = try manager.databaseQueue.read { db in + let countsAfter = try manager.databaseConnection.read { db in return ( sites: try TestSite.filter(Column("id") == testSiteId).fetchCount(db), products: try TestProduct.filter(Column("siteID") == testSiteId).fetchCount(db), @@ -334,13 +334,13 @@ struct GRDBManagerTests { @Test("Fetch products by site") func test_can_fetch_products_by_site() throws { // Given - Create second site and products for both sites - let site2Id = try manager.databaseQueue.write { db -> Int64 in + let site2Id = try manager.databaseConnection.write { db -> Int64 in let site = TestSite(id: 2) try site.insert(db) return 2 } - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in // Site 1 products for i in 1...3 { let product = TestProduct( @@ -371,13 +371,13 @@ struct GRDBManagerTests { } // When - let site1Products = try manager.databaseQueue.read { db in + let site1Products = try manager.databaseConnection.read { db in try TestProduct .filter(Column("siteID") == sampleSiteID) .fetchAll(db) } - let site2Products = try manager.databaseQueue.read { db in + let site2Products = try manager.databaseConnection.read { db in try TestProduct .filter(Column("siteID") == site2Id) .fetchAll(db) @@ -397,7 +397,7 @@ struct GRDBManagerTests { @Test("Fetch variations by site") func test_can_fetch_variations_by_site() throws { // Given - Product and variations - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let product = TestProduct( siteID: sampleSiteID, id: 100, @@ -422,7 +422,7 @@ struct GRDBManagerTests { } // When - let variations = try manager.databaseQueue.read { db in + let variations = try manager.databaseConnection.read { db in try TestProductVariation .filter(Column("siteID") == sampleSiteID) .fetchAll(db) @@ -438,7 +438,7 @@ struct GRDBManagerTests { func test_cannot_insert_product_without_valid_site() throws { // When/Then #expect(throws: DatabaseError.self) { - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let product = TestProduct( siteID: 999, // Non-existent site id: 100, @@ -457,7 +457,7 @@ struct GRDBManagerTests { func test_cannot_insert_variation_without_valid_product() throws { // When/Then #expect(throws: DatabaseError.self) { - try manager.databaseQueue.write { db in + try manager.databaseConnection.write { db in let variation = TestProductVariation( siteID: sampleSiteID, id: 200, diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 23492bdb61b..febef419366 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -4,6 +4,7 @@ import SwiftUI import Yosemite import class WooFoundation.CurrencySettings import protocol Storage.StorageManagerType +import protocol Storage.GRDBManagerProtocol /// View controller that provides the tab bar item for the Point of Sale tab. /// It is never visible on the screen, only used to provide the tab bar item as all POS UI is full-screen. @@ -26,6 +27,7 @@ final class POSTabCoordinator { private let storesManager: StoresManager private let credentials: Credentials? private let storageManager: StorageManagerType + private let grdbManager: GRDBManagerProtocol? private let currencySettings: CurrencySettings private let pushNotesManager: PushNotesManager private let eligibilityChecker: POSEntryPointEligibilityCheckerProtocol @@ -77,6 +79,13 @@ final class POSTabCoordinator { self.eligibilityChecker = eligibilityChecker tabContainerController.wrappedController = POSTabViewController() + + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) { + self.grdbManager = ServiceLocator.grdbManager + logDatabaseSchema() + } else { + self.grdbManager = nil + } } func onTabSelected() { @@ -155,3 +164,11 @@ private extension POSTabCoordinator { TracksProvider.setPOSMode(isPointOfSaleActive) } } + +private extension POSTabCoordinator { + func logDatabaseSchema() { + try? grdbManager?.databaseConnection.read { db in + return try db.dumpSchema() + } + } +} diff --git a/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift b/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift index 6fec03c0396..fc0c27ca33f 100644 --- a/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift +++ b/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift @@ -61,6 +61,10 @@ final class ServiceLocator { /// private static var _storageManager = CoreDataManager(name: WooConstants.databaseStackName, crashLogger: crashLogging) + /// GRDB Manager for local catalog persistence + /// + private static var _grdbManager: GRDBManagerProtocol? + /// Cocoalumberjack DDLog /// private static var _fileLogger: Logs = DDFileLogger() @@ -191,6 +195,35 @@ final class ServiceLocator { return _storageManager } + /// Provides the access point to the GRDBManager for local catalog persistence. + /// - Returns: An instance of GRDBManagerProtocol when the pointOfSaleLocalCatalogi1 feature flag is enabled + /// - Throws: Fatal error if GRDBManager initialization fails + static var grdbManager: GRDBManagerProtocol { + guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) else { + fatalError("GRDBManager accessed when pointOfSaleLocalCatalogi1 feature flag is disabled") + } + + guard let grdbManager = _grdbManager else { + do { + guard let documentsPath = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask).first else { + fatalError("Failed to get the path to the documents directory.") + } + + let databasePath = documentsPath.appendingPathComponent(WooConstants.localSQLiteDatabaseName).path + let manager = try GRDBManager(databasePath: databasePath) + DDLogInfo("Started GRDBManager with database path: \(databasePath)") + _grdbManager = manager + return manager + } catch { + fatalError("Failed to initialize GRDBManager: \(error)") + } + } + + return grdbManager + } + /// Provides the access point to the FileLogger. /// - Returns: An implementation of the Logs protocol. It defaults to DDFileLogger static var fileLogger: Logs { @@ -417,6 +450,15 @@ extension ServiceLocator { _productImageUploader = mock } + + /// periphery:ignore - for use in future tests. + static func setGRDBManager(_ testInstance: GRDBManagerProtocol) { + guard isRunningTests() else { + return + } + + _grdbManager = testInstance + } } diff --git a/WooCommerce/Classes/System/WooConstants.swift b/WooCommerce/Classes/System/WooConstants.swift index 6c03af55ab2..40e3b0aae1a 100644 --- a/WooCommerce/Classes/System/WooConstants.swift +++ b/WooCommerce/Classes/System/WooConstants.swift @@ -13,6 +13,10 @@ public enum WooConstants { /// static let databaseStackName = "WooCommerce" + /// Local SQLite Database Name + /// + static let localSQLiteDatabaseName = "woo-local.sqlite" + /// Keychain Access's Service Name /// public static let keychainServiceName = "com.automattic.woocommerce"