// // main.swift // PGSQLDataStore // // Created by Duncan Groenewald on 1/9/2026. // import Foundation import SwiftData import PostgresNIO import Logging // ========================================== // 1. DATA MODEL SCHEMA // ========================================== @Model final class ProductRecord { @Attribute(.unique) var id: UUID = UUID() var sku: String = "" var stockQuantity: Int = 0 init(id: UUID = UUID(), sku: String, stockQuantity: Int) { self.id = id self.sku = sku self.stockQuantity = stockQuantity } enum CodingKeys: String, CodingKey { case id, sku, stockQuantity } required init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(UUID.self, forKey: .id) self.sku = try container.decode(String.self, forKey: .sku) self.stockQuantity = try container.decode(Int.self, forKey: .stockQuantity) } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(sku, forKey: .sku) try container.encode(stockQuantity, forKey: .stockQuantity) } } @Model final class SupplierRecord { @Attribute(.unique) var id: UUID = UUID() @Attribute(.unique) var name: String = "" init(id: UUID = UUID(), name: String) { self.id = id self.name = name } } enum AppError: Error { case runtimeError(String) } // ========================================== // 2. CUSTOM SNAPSHOT // ========================================== struct ProductPostgresSnapshot: DataStoreSnapshot { // Required protocol property identifier var persistentIdentifier: PersistentIdentifier // Strict column fields matching PostgreSQL definitions var id: UUID var sku: String var stockQuantity: Int // Requirement A: Lifecycle initializer used by SwiftData to pull out internal model states init(from backingData: any BackingData, relatedBackingDatas: inout [PersistentIdentifier : any BackingData]) { // Recover or safely establish the identity block self.persistentIdentifier = backingData.persistentModelID! // Dynamically extract model values from the type-erased BackingData container if let userBacking = backingData as? (any BackingData) { self.id = userBacking.getValue(forKey: \ProductRecord.id) self.sku = userBacking.getValue(forKey: \ProductRecord.sku) self.stockQuantity = userBacking.getValue(forKey: \ProductRecord.stockQuantity) } else { // Safe fallbacks for edge-case corruption payloads self.id = UUID() self.sku = "UNKNOWN" self.stockQuantity = 0 } } // Requirement B: Identifiers copier used during transaction insertions and cache remappings func copy(persistentIdentifier: PersistentIdentifier, remappedIdentifiers: [PersistentIdentifier : PersistentIdentifier]? = nil) -> ProductPostgresSnapshot { return ProductPostgresSnapshot( persistentIdentifier: persistentIdentifier, id: self.id, sku: self.sku, stockQuantity: self.stockQuantity ) } enum CodingKeys: String, CodingKey { case persistentIdentifier, id, sku, stockQuantity } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.persistentIdentifier = try container.decode(PersistentIdentifier.self, forKey: .persistentIdentifier) self.id = try container.decode(UUID.self, forKey: .id) self.sku = try container.decode(String.self, forKey: .sku) self.stockQuantity = try container.decode(Int.self, forKey: .stockQuantity) } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(persistentIdentifier, forKey: .persistentIdentifier) try container.encode(id, forKey: .id) try container.encode(sku, forKey: .sku) try container.encode(stockQuantity, forKey: .stockQuantity) } // Custom driver initializer utilized for building records dynamically from PostgresNIO rows init(persistentIdentifier: PersistentIdentifier, id: UUID, sku: String, stockQuantity: Int) { self.persistentIdentifier = persistentIdentifier self.id = id self.sku = sku self.stockQuantity = stockQuantity } } // ========================================== // 3. POSTGRESNIO STORES CONFIGURATION // ========================================== struct PostgresStoreConfiguration: DataStoreConfiguration { typealias Store = PostgresSwiftDataStore var name: String var schema: Schema? var connectionOptions: PostgresConnection.Configuration } extension PostgresStoreConfiguration: Equatable, Hashable { static func == (lhs: PostgresStoreConfiguration, rhs: PostgresStoreConfiguration) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } // ========================================== // 4. CUSTOM COMPLIANT DATASTORE ENGINE // ========================================== final class PostgresSwiftDataStore: DataStore { let schema: Schema = Schema([ProductRecord.self], version: Schema.Version(1, 0, 0)) typealias Configuration = PostgresStoreConfiguration typealias Snapshot = ProductPostgresSnapshot let configuration: PostgresStoreConfiguration var connection: PostgresConnection? = nil let logger = Logger(label: "com.app.postgres.datastore") var identifier: String { "PostgresSwiftDataStore-\(configuration.name)" } init(_ configuration: PostgresStoreConfiguration, migrationPlan: (any SchemaMigrationPlan.Type)?) throws { self.configuration = configuration let semaphore = DispatchSemaphore(value: 0) Task { do { // Bootstrapping PostgresNIO wire framework connection self.connection = try await PostgresConnection.connect( on: MultiThreadedEventLoopGroup.singleton.any(), configuration: configuration.connectionOptions, id: 1, logger: self.logger ) } catch { } semaphore.signal() } semaphore.wait() } deinit { self.close() } func close() { let semaphore = DispatchSemaphore(value: 0) Task { try? await self.connection?.close() semaphore.signal() } semaphore.wait() } // Saves changes by parsing individual items inside the save request bundle func save(_ request: DataStoreSaveChangesRequest) throws -> DataStoreSaveChangesResult { guard let connection = self.connection else { throw AppError.runtimeError("No connection to database") } let semaphore = DispatchSemaphore(value: 0) var remappedIdentifiers: [PersistentIdentifier: PersistentIdentifier] = [:] Task { var remappedIdentifiersX: [PersistentIdentifier: PersistentIdentifier] = [:] do { // Process Insertions for snapshot in request.inserted { print("\(snapshot.persistentIdentifier.storeIdentifier ?? "nil"), \(snapshot.persistentIdentifier.entityName), \(snapshot.persistentIdentifier.id)") let pId = try PersistentIdentifier.identifier(for: identifier, entityName: "ProductRecord", primaryKey: snapshot.id) remappedIdentifiersX[snapshot.persistentIdentifier] = pId try await connection.query( """ INSERT INTO products (id, sku, stockquantity) VALUES (\(snapshot.id), \(snapshot.sku), \(snapshot.stockQuantity)); """, logger: logger ) } // Process Modifications (Updates) for snapshot in request.updated { try await connection.query( """ UPDATE items_inventory SET sku = \(snapshot.sku), stockquantity = \(snapshot.stockQuantity) WHERE id = \(snapshot.id); """, logger: logger ) } // Process Record Deletions for snapshot in request.deleted { try await connection.query( "DELETE FROM products WHERE id = \(snapshot.id);", logger: logger ) } } catch { logger.error("Failed executing batch change block: \(error)") } remappedIdentifiers = remappedIdentifiersX semaphore.signal() } semaphore.wait() return DataStoreSaveChangesResult(for: self.identifier, remappedIdentifiers: remappedIdentifiers) } // Reads out records and hydrates fresh snapshot frames directly func fetch(_ request: DataStoreFetchRequest) throws -> DataStoreFetchResult where T : PersistentModel { guard let connection = self.connection else { throw AppError.runtimeError("No connection to database") } var collectedSnapshots: [ProductPostgresSnapshot] = [] let semaphore = DispatchSemaphore(value: 0) Task { do { let rows = try await connection.query( "SELECT id, sku, stockquantity FROM products;", logger: logger ) // Decode rows directly into tuples via PostgresNIO for try await (dbID, dbSku, dbStock) in rows.decode((UUID, String, Int).self) { let persistentID = try PersistentIdentifier.identifier(for: identifier, entityName: "ProductRecord", primaryKey: dbID) let snapshot = ProductPostgresSnapshot( persistentIdentifier: persistentID, id: dbID, sku: dbSku, stockQuantity: dbStock ) collectedSnapshots.append(snapshot) } } catch { logger.error("Data tracking stream operation caught error: \(error)") } semaphore.signal() } semaphore.wait() return DataStoreFetchResult(descriptor: request.descriptor, fetchedSnapshots: collectedSnapshots) } } struct Credentials { var username: String var password: String } // ========================================== // 5. RUNTIME INITIALIZER // ========================================== struct InventoryApp { static let hostname: String = "localhost" static let username: String = "swiftdatastore" static let databasename: String = "swiftdatastore" static let port: Int = 5432 static let password: String = "swiftdatastore" static func main() async { print("🚀 Initializing Protocol-Compliant PostgresNIO + SwiftData Core Context...") let credentials = Credentials(username: username, password: password) var tlsConfiguration = TLSConfiguration.makeClientConfiguration() tlsConfiguration.certificateVerification = .none let config = PostgresClient.Configuration( host: hostname, port: port, username: username, password: credentials.password, database: databasename, tls: .prefer(tlsConfiguration) ) let storeConfig = PostgresStoreConfiguration( name: "ProductionInventory", schema: Schema([ProductRecord.self]), connectionOptions: try! createPgConConfiguration() ) do { // Instantiate our updated custom store natively into the model container let customContainer = try ModelContainer(for: ProductRecord.self, configurations: storeConfig) let context = ModelContext(customContainer) // --- INSERTS TESTING --- print("💾 Saving a new record into SwiftData context...") let newProduct = ProductRecord(sku: "IPHONE-16-PRO", stockQuantity: 20) context.insert(newProduct) try context.save() let newProduct2 = ProductRecord(sku: "IPHONE-15-PRO", stockQuantity: 50) context.insert(newProduct2) try context.save() // --- FETCHES TESTING --- print("🔍 Requesting collection from PostgreSQL target tables...") let fetchDescriptor = FetchDescriptor() let localRecords = try context.fetch(fetchDescriptor) for item in localRecords { //print("📦 Mapped Record -> SKU: \(item.sku) | Stock: \(item.stockQuantity) | Identifier: \(item.id.uuidString)") print("📦 Mapped Record -> Identifier: \(item.id.uuidString) | SKU: \(item.sku) | Stock: \(item.stockQuantity)") } } catch { print("🛑 Fatal exception caught in pipeline execution: \(error)") } } static func createPgConConfiguration() throws -> PostgresConnection.Configuration { let credentials = Credentials(username: username, password: password) var tlsConfiguration = TLSConfiguration.makeClientConfiguration() tlsConfiguration.certificateVerification = .none let sslContext = try! NIOSSLContext(configuration: tlsConfiguration) //var tlsConfiguration = TLSConfiguration.makeClientConfiguration() let config = PostgresConnection.Configuration( host: hostname, port: port, username: username, password: credentials.password, database: databasename, tls: .prefer(sslContext) ) return config } } await InventoryApp.main() /* DATABASE SCHEMA */ /* CREATE TABLE swiftdatastore.products ( id UUID PRIMARY KEY NOT NULL DEFAULT (gen_random_uuid()), tstamp timestamp with time zone DeFAULT CURRENT_TIMESTAMP, sku text NOT NULL, stockQuantity int DEFAULT 0, UNIQUE (sku) ); */