From 9114e4f792d0bd91f7dfe74f1e32711826b8138b Mon Sep 17 00:00:00 2001 From: Muukii Date: Mon, 28 Aug 2023 01:38:27 +0900 Subject: [PATCH 01/16] :zap: WIP --- Sources/VergeMacrosPlugin/DatabaseMacro.swift | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Sources/VergeMacrosPlugin/DatabaseMacro.swift diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/DatabaseMacro.swift new file mode 100644 index 0000000000..f89faeb57f --- /dev/null +++ b/Sources/VergeMacrosPlugin/DatabaseMacro.swift @@ -0,0 +1,5 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + + From 393ed52fd0960512b12084d9a84c64cc785ade96 Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 30 Aug 2023 03:14:30 +0900 Subject: [PATCH 02/16] :evergreen_tree: Update --- Package.swift | 9 + Sources/VergeMacrosPlugin/DatabaseMacro.swift | 225 ++++++++++++++++++ Sources/VergeMacrosPlugin/MacroError.swift | 17 ++ Sources/VergeMacrosPlugin/Plugin.swift | 4 + .../EntityType.swift | 19 +- .../VergeNormalization/ModifyingTable.swift | 8 + .../VergeNormalization/NonAtomicCounter.swift | 12 + .../NormalizedStorageType.swift | 73 ++++++ .../PrimitiveIdentifier.swift | 0 Sources/VergeNormalization/Table.swift | 149 ++++++++++++ .../VergeNormalization+Macros.swift | 36 +++ .../VergeNormalization.swift | 1 + Sources/VergeORM/EntityTablesStorage.swift | 4 +- Sources/VergeORM/EntityType+ORM.swift | 4 + .../VergeORM/Index/GroupByEntityIndex.swift | 2 + Sources/VergeORM/Index/HashIndex.swift | 2 + Sources/VergeORM/Index/OrderedIDIndex.swift | 1 + Sources/VergeORM/Index/SetIndex.swift | 2 + .../{macros.swift => VergeORM+macros.swift} | 0 Sources/VergeORM/VergeORM.swift | 4 + .../VergeMacrosTests/DatabaseMacroTests.swift | 57 +++++ 21 files changed, 610 insertions(+), 19 deletions(-) create mode 100644 Sources/VergeMacrosPlugin/MacroError.swift rename Sources/{VergeORM => VergeNormalization}/EntityType.swift (87%) create mode 100644 Sources/VergeNormalization/ModifyingTable.swift create mode 100644 Sources/VergeNormalization/NonAtomicCounter.swift create mode 100644 Sources/VergeNormalization/NormalizedStorageType.swift rename Sources/{VergeORM => VergeNormalization}/PrimitiveIdentifier.swift (100%) create mode 100644 Sources/VergeNormalization/Table.swift create mode 100644 Sources/VergeNormalization/VergeNormalization+Macros.swift create mode 100644 Sources/VergeNormalization/VergeNormalization.swift create mode 100644 Sources/VergeORM/EntityType+ORM.swift rename Sources/VergeORM/{macros.swift => VergeORM+macros.swift} (100%) create mode 100644 Sources/VergeORM/VergeORM.swift create mode 100644 Tests/VergeMacrosTests/DatabaseMacroTests.swift diff --git a/Package.swift b/Package.swift index f9aeb5df57..5dab84e09a 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,7 @@ let package = Package( .library(name: "Verge", targets: ["Verge"]), .library(name: "VergeTiny", targets: ["VergeTiny"]), .library(name: "VergeORM", targets: ["VergeORM"]), + .library(name: "VergeNormalization", targets: ["VergeNormalization"]), .library(name: "VergeRx", targets: ["VergeRx"]), .library(name: "VergeClassic", targets: ["VergeClassic"]), .library(name: "VergeMacros", targets: ["VergeMacros"]), @@ -59,9 +60,17 @@ let package = Package( "VergeRx" ] ), + .target( + name: "VergeNormalization", + dependencies: [ + "Verge", + .product(name: "HashTreeCollections", package: "swift-collections"), + ] + ), .target( name: "VergeORM", dependencies: [ + "VergeNormalization", "Verge", .product(name: "HashTreeCollections", package: "swift-collections"), ] diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/DatabaseMacro.swift index f89faeb57f..f5f26ce0ae 100644 --- a/Sources/VergeMacrosPlugin/DatabaseMacro.swift +++ b/Sources/VergeMacrosPlugin/DatabaseMacro.swift @@ -2,4 +2,229 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros +public struct DatabaseMacro: Macro { +} + +extension DatabaseMacro: ExtensionMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, + providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, + conformingTo protocols: [SwiftSyntax.TypeSyntax], + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { + + guard let structDecl = declaration.as(StructDeclSyntax.self) else { + fatalError() + } + + return [ + (""" + extension \(structDecl.name.trimmed): NormalizedStorageType {} + """ as DeclSyntax).cast(ExtensionDeclSyntax.self), + (""" + extension \(structDecl.name.trimmed): Equatable {} + """ as DeclSyntax).cast(ExtensionDeclSyntax.self), + (""" + extension \(structDecl.name.trimmed) { + typealias BBB = String + struct Context {} + } + """ as DeclSyntax).cast(ExtensionDeclSyntax.self) + ] + } + +} + +//extension DatabaseMacro: PeerMacro { +// public static func expansion( +// of node: SwiftSyntax.AttributeSyntax, +// providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, +// in context: some SwiftSyntaxMacros.MacroExpansionContext +// ) throws -> [SwiftSyntax.DeclSyntax] { +// +// guard let structDecl = declaration.as(StructDeclSyntax.self) else { +// fatalError() +// } +// +// return [ +// """ +//extension \(structDecl.name.trimmed) { +// struct A {} +//} +//""" +// ] +// } +// +//} + +extension DatabaseMacro: DeclarationMacro { + public static func expansion( + of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + return [] + } +} + +/// Add @Table +extension DatabaseMacro: MemberAttributeMacro { + + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, + providingAttributesFor member: some SwiftSyntax.DeclSyntaxProtocol, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.AttributeSyntax] { + + if let variableDecl = member.as(VariableDeclSyntax.self) { + + let isGenerated = variableDecl + .bindings + .allSatisfy { + $0.cast(PatternBindingSyntax.self).pattern.cast(IdentifierPatternSyntax.self).identifier + .description.hasPrefix("_$") + } + + if isGenerated { + return [] + } + + // if isComputedProperty(from: variableDecl) { + // return [] + // } + + return [ + "@Table" + ] + + } + + return [] + + } + +} + +/// Add member +extension DatabaseMacro: MemberMacro { + + final class RenamingVisitor: SyntaxRewriter { + + init() {} + + override func visit(_ node: IdentifierPatternSyntax) -> PatternSyntax { + return "_$\(node.identifier)" + } + + override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { + + // TODO: make variable private + return super.visit(node) + } + } + + final class StoredPropertyCollector: SyntaxVisitor { + + var storedProperties: [VariableDeclSyntax] = [] + + var onFoundMultipleBindings: () -> Void = {} + + override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { + + if node.bindingSpecifier == "let" { + storedProperties.append(node) + return super.visit(node) + } + + if node.bindings.count > 1 { + // let a,b,c = 0 + // it's stored + onFoundMultipleBindings() + return super.visit(node) + } + + if node.bindings.first?.accessorBlock == nil { + storedProperties.append(node) + return super.visit(node) + } + + // computed property + + return .visitChildren + } + + } + + static func makeVariableFromConstant(_ node: VariableDeclSyntax) -> VariableDeclSyntax { + var modified = node + modified.bindingSpecifier = "var" + return modified + } + + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + + + let v = StoredPropertyCollector(viewMode: .fixedUp) + v.onFoundMultipleBindings = { + context.addDiagnostics(from: MacroError(message: "Cannot use multiple bindings"), node: node) + } + v.walk(declaration.memberBlock) + + let storageMembers = v.storedProperties + .map(makeVariableFromConstant) + .map { + + let rename = RenamingVisitor() + let renamed = rename.visit($0) + + return renamed + } + + return storageMembers + + } + +} + +public struct DatabaseTableMacro: Macro { + +} + +extension DatabaseTableMacro: AccessorMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.AccessorDeclSyntax] { + + func identifier(from node: VariableDeclSyntax) -> TokenSyntax { + node.bindings.first!.cast(PatternBindingSyntax.self).pattern.cast(IdentifierPatternSyntax.self).identifier + } + + let id = identifier(from: declaration.cast(VariableDeclSyntax.self)) + + return [ + """ + get { + _$\(id) + } + """, + """ + set { + _$\(id) = newValue + } + """ , + ] + } + +} + +public struct DatabaseIndexMacro: Macro { + +} diff --git a/Sources/VergeMacrosPlugin/MacroError.swift b/Sources/VergeMacrosPlugin/MacroError.swift new file mode 100644 index 0000000000..4a0ccfb1a3 --- /dev/null +++ b/Sources/VergeMacrosPlugin/MacroError.swift @@ -0,0 +1,17 @@ + +import SwiftDiagnostics + +public struct MacroError: Error, DiagnosticMessage { + + public var message: String + + public var diagnosticID: SwiftDiagnostics.MessageID { + .init(domain: "Verge", id: "MacroError") + } + + public var severity: SwiftDiagnostics.DiagnosticSeverity = .error + + init(message: String) { + self.message = message + } +} diff --git a/Sources/VergeMacrosPlugin/Plugin.swift b/Sources/VergeMacrosPlugin/Plugin.swift index 32f0415087..75e51f2924 100644 --- a/Sources/VergeMacrosPlugin/Plugin.swift +++ b/Sources/VergeMacrosPlugin/Plugin.swift @@ -8,5 +8,9 @@ struct Plugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ DatabaseStateMacro.self, IfChangedMacro.self, + + DatabaseMacro.self, + DatabaseTableMacro.self, + DatabaseIndexMacro.self ] } diff --git a/Sources/VergeORM/EntityType.swift b/Sources/VergeNormalization/EntityType.swift similarity index 87% rename from Sources/VergeORM/EntityType.swift rename to Sources/VergeNormalization/EntityType.swift index 074742ed4d..865218f3b9 100644 --- a/Sources/VergeORM/EntityType.swift +++ b/Sources/VergeNormalization/EntityType.swift @@ -42,7 +42,7 @@ public struct EntityIdentifier : Hashable, CustomStringConve raw.hash(into: &hasher) } - let any: AnyEntityIdentifier + public let any: AnyEntityIdentifier public let raw: Entity.EntityIDRawType @@ -51,7 +51,8 @@ public struct EntityIdentifier : Hashable, CustomStringConve self.any = .init(raw._primitiveIdentifier) } - init(_ anyIdentifier: AnyEntityIdentifier) { + @_spi(ForORM) + public init(_ anyIdentifier: AnyEntityIdentifier) { self.any = anyIdentifier self.raw = Entity.EntityIDRawType._restore(from: anyIdentifier.value)! } @@ -73,12 +74,6 @@ public protocol EntityType: Equatable, Sendable { var entityID: EntityID { get } - #if COCOAPODS - typealias EntityTableKey = Verge.EntityTableKey - #else - typealias EntityTableKey = VergeORM.EntityTableKey - #endif - typealias EntityID = EntityIdentifier } @@ -92,14 +87,6 @@ extension EntityType { public static var entityName: EntityTableIdentifier { .init(Self.self) } - - @available(*, deprecated, renamed: "EntityID") - public typealias ID = EntityID - - @available(*, deprecated, renamed: "entityID") - public var id: EntityID { - _read { yield entityID } - } } diff --git a/Sources/VergeNormalization/ModifyingTable.swift b/Sources/VergeNormalization/ModifyingTable.swift new file mode 100644 index 0000000000..b66746f472 --- /dev/null +++ b/Sources/VergeNormalization/ModifyingTable.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Muukii on 2023/08/30. +// + +import Foundation diff --git a/Sources/VergeNormalization/NonAtomicCounter.swift b/Sources/VergeNormalization/NonAtomicCounter.swift new file mode 100644 index 0000000000..c4bc392290 --- /dev/null +++ b/Sources/VergeNormalization/NonAtomicCounter.swift @@ -0,0 +1,12 @@ +/// A container that manages raw value to describe mark as updated. +public struct NonAtomicCounter: Hashable, Sendable { + + private(set) public var value: UInt64 = 0 + + public init() {} + + public consuming func increment() { + value &+= 1 + } + +} diff --git a/Sources/VergeNormalization/NormalizedStorageType.swift b/Sources/VergeNormalization/NormalizedStorageType.swift new file mode 100644 index 0000000000..2b954b8270 --- /dev/null +++ b/Sources/VergeNormalization/NormalizedStorageType.swift @@ -0,0 +1,73 @@ + +public protocol NormalizedStorageType { + + func finalizeTransaction(transaction: inout ModifyingTransaction) + +} + +extension NormalizedStorageType { + public func finalizeTransaction(transaction: inout ModifyingTransaction) { + + } +} + +extension NormalizedStorageType { + + public func beginBatchUpdates() -> ModifyingTransaction { + let context = ModifyingTransaction(target: self) + return context + } + + public consuming func commitBatchUpdates(transaction: consuming ModifyingTransaction) { + + // middlewareAfter + do { + finalizeTransaction(transaction: &transaction) + } + + // apply + do { + self = transaction.modifying + } + + } + + /// Performs operations to update entities and indexes + /// If can be run on background thread with locking. + /// + /// - Parameter update: + @discardableResult + public mutating func performBatchUpdates(_ update: (inout ModifyingTransaction) throws -> Result) rethrows -> Result { + do { + var transaction = beginBatchUpdates() + let result = try update(&transaction) + commitBatchUpdates(transaction: transaction) + return result + } catch { + throw error + } + } +} + +public enum ModifyingTransactionError: Error { + case aborted + case storedEntityNotFound +} + +public struct ModifyingTransaction: ~Copyable { + + public let current: NormalizedStorage + + public var modifying: NormalizedStorage + + init(target: NormalizedStorage) { + self.current = target + self.modifying = target + } + + /// raises an error + public func abort() throws -> Never { + throw ModifyingTransactionError.aborted + } +} + diff --git a/Sources/VergeORM/PrimitiveIdentifier.swift b/Sources/VergeNormalization/PrimitiveIdentifier.swift similarity index 100% rename from Sources/VergeORM/PrimitiveIdentifier.swift rename to Sources/VergeNormalization/PrimitiveIdentifier.swift diff --git a/Sources/VergeNormalization/Table.swift b/Sources/VergeNormalization/Table.swift new file mode 100644 index 0000000000..24dfe09190 --- /dev/null +++ b/Sources/VergeNormalization/Table.swift @@ -0,0 +1,149 @@ +import struct HashTreeCollections.TreeDictionary + +public struct Table: Equatable { + + private var storage: TreeDictionary + private(set) var updatedMarker = NonAtomicCounter() + + /// The number of entities in table + public var count: Int { + _read { yield storage.count } + } + + /// A Boolean value that indicates whether the dictionary is empty. + public var isEmpty: Bool { + _read { yield storage.isEmpty } + } + + public let identifier: String + + init(identifier: String, entities: TreeDictionary = .init()) { + self.identifier = identifier + self.storage = entities + } + + /// Returns all entity ids that stored. + public borrowing func allIDs() -> TreeDictionary.Keys { + return storage.keys + } + + /// Returns all entity that stored. + public borrowing func allEntities() -> some Collection { + return storage + } + + /** + Finds an entity by the identifier of the entity. + - Returns: An entity that found by identifier. Nil if the table does not have that entity. + */ + public borrowing func find(by id: Entity.EntityID) -> Entity? { + return storage[id] + } + + /// Finds entities by set of ids. + /// The order of array would not be sorted, it depends on dictionary's buffer. + /// + /// if ids contains same id, result also contains same element. + /// - Parameter ids: sequence of Entity.ID + public borrowing func find(in ids: S) -> [Entity] where S.Element == Entity.EntityID { + + return ids.reduce(into: [Entity]()) { (buf, id) in + guard let entity = storage[id] else { return } + buf.append(entity) + } + } + + /** + Updates the entity that already exsisting in the table. + + - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) + */ + @discardableResult + @inline(__always) + public consuming func updateExists( + id: Entity.EntityID, + update: (inout Entity) throws -> Void + ) throws -> Entity { + + guard var current = storage[id] else { + throw ModifyingTransactionError.storedEntityNotFound + } + + try update(¤t) + precondition(current.entityID == id) + storage[id] = current + + updatedMarker.increment() + + return current + } + + /** + Updates the entity that already exsisting in the table. + + - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) + */ + @discardableResult + public consuming func updateIfExists( + id: Entity.EntityID, + update: (inout Entity) throws -> Void + ) rethrows -> Entity? { + try? updateExists(id: id, update: update) + } + + /** + Inserts an entity + */ + @discardableResult + public consuming func insert(_ entity: Entity) -> InsertionResult { + + storage[entity.entityID] = entity + + updatedMarker.increment() + + return .init(entity: entity) + } + + /** + Inserts a collection of the entity. + */ + @discardableResult + public consuming func insert(_ addingEntities: S) -> [InsertionResult] where S.Element == Entity { + + let results = addingEntities.map { entity -> InsertionResult in + storage[entity.entityID] = entity + return .init(entity: entity) + } + + updatedMarker.increment() + + return results + } + + /** + Removes the entity by the identifier. + */ + public consuming func remove(_ id: Entity.EntityID) { + storage.removeValue(forKey: id) + updatedMarker.increment() + } + + /** + Removes the all of the entities in the table. + */ + public consuming func removeAll() { + storage.removeAll(where: { _ in true }) + updatedMarker.increment() + } +} + +extension Table { + /// An object indicates result of insertion + /// It can be used to create a getter object. + public struct InsertionResult { + public var entityID: Entity.EntityID { + entity.entityID + } + public let entity: Entity + } +} diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift new file mode 100644 index 0000000000..d470d89f2d --- /dev/null +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -0,0 +1,36 @@ + +@attached(member, names: arbitrary) +@attached(memberAttribute) +@attached(extension, conformances: NormalizedStorageType, Equatable, names: named(Context), named(BBB)) +public macro Database() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseMacro") + +@attached(accessor) +public macro Table() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseTableMacro") + +#if DEBUG + +struct A: EntityType { + typealias EntityIDRawType = String + var entityID: EntityID { + .init("") + } +} + +@Database +struct MyDatabase { + var user: Table +} + +private func play() { + + var db = MyDatabase(_$user: .init(identifier: "")) + + db.performBatchUpdates { t in + t.modifying.user.insert(.init()) + } + +// db.user = .init(identifier: "") +} +//#Database(tables: Table(), Table()) + +#endif diff --git a/Sources/VergeNormalization/VergeNormalization.swift b/Sources/VergeNormalization/VergeNormalization.swift new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Sources/VergeNormalization/VergeNormalization.swift @@ -0,0 +1 @@ + diff --git a/Sources/VergeORM/EntityTablesStorage.swift b/Sources/VergeORM/EntityTablesStorage.swift index c04f1c8927..2a1db0948c 100644 --- a/Sources/VergeORM/EntityTablesStorage.swift +++ b/Sources/VergeORM/EntityTablesStorage.swift @@ -21,10 +21,8 @@ import Foundation import struct HashTreeCollections.TreeDictionary - -#if !COCOAPODS +@_spi(ForORM) import struct VergeNormalization.EntityIdentifier import Verge -#endif protocol _EntityTableType { typealias RawTable = _EntityRawTable diff --git a/Sources/VergeORM/EntityType+ORM.swift b/Sources/VergeORM/EntityType+ORM.swift new file mode 100644 index 0000000000..fa6ab2a901 --- /dev/null +++ b/Sources/VergeORM/EntityType+ORM.swift @@ -0,0 +1,4 @@ + +extension EntityType { + public typealias EntityTableKey = VergeORM.EntityTableKey +} diff --git a/Sources/VergeORM/Index/GroupByEntityIndex.swift b/Sources/VergeORM/Index/GroupByEntityIndex.swift index d8b78b75de..dc71801539 100644 --- a/Sources/VergeORM/Index/GroupByEntityIndex.swift +++ b/Sources/VergeORM/Index/GroupByEntityIndex.swift @@ -21,6 +21,8 @@ import Foundation +@_spi(ForORM) import struct VergeNormalization.EntityIdentifier + @available(*, deprecated, renamed: "GroupByEntityIndex") public typealias GroupByIndex< Schema: EntitySchemaType, diff --git a/Sources/VergeORM/Index/HashIndex.swift b/Sources/VergeORM/Index/HashIndex.swift index 624ae4e37a..b6ad9ac2f5 100644 --- a/Sources/VergeORM/Index/HashIndex.swift +++ b/Sources/VergeORM/Index/HashIndex.swift @@ -20,6 +20,8 @@ // THE SOFTWARE. import Foundation +@_spi(ForORM) import struct VergeNormalization.EntityIdentifier + /// Dictionary based index storage public struct HashIndex: IndexType, Equatable { diff --git a/Sources/VergeORM/Index/OrderedIDIndex.swift b/Sources/VergeORM/Index/OrderedIDIndex.swift index 3009e2148b..2cc9e5c769 100644 --- a/Sources/VergeORM/Index/OrderedIDIndex.swift +++ b/Sources/VergeORM/Index/OrderedIDIndex.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. import Foundation +@_spi(ForORM) import struct VergeNormalization.EntityIdentifier /// Ordered Collection based index storage public struct OrderedIDIndex: IndexType, Equatable { diff --git a/Sources/VergeORM/Index/SetIndex.swift b/Sources/VergeORM/Index/SetIndex.swift index 6612ee2ee6..6db8990d82 100644 --- a/Sources/VergeORM/Index/SetIndex.swift +++ b/Sources/VergeORM/Index/SetIndex.swift @@ -21,6 +21,8 @@ import Foundation +@_spi(ForORM) import struct VergeNormalization.EntityIdentifier + /// Ordered Collection based index storage public struct SetIndex: IndexType, Equatable { diff --git a/Sources/VergeORM/macros.swift b/Sources/VergeORM/VergeORM+macros.swift similarity index 100% rename from Sources/VergeORM/macros.swift rename to Sources/VergeORM/VergeORM+macros.swift diff --git a/Sources/VergeORM/VergeORM.swift b/Sources/VergeORM/VergeORM.swift new file mode 100644 index 0000000000..b93865d76e --- /dev/null +++ b/Sources/VergeORM/VergeORM.swift @@ -0,0 +1,4 @@ + +@_exported import protocol VergeNormalization.EntityType +@_exported import struct VergeNormalization.AnyEntityIdentifier +@_exported import struct VergeNormalization.EntityTableIdentifier diff --git a/Tests/VergeMacrosTests/DatabaseMacroTests.swift b/Tests/VergeMacrosTests/DatabaseMacroTests.swift new file mode 100644 index 0000000000..f37b8c00af --- /dev/null +++ b/Tests/VergeMacrosTests/DatabaseMacroTests.swift @@ -0,0 +1,57 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +#if canImport(VergeMacrosPlugin) +import VergeMacrosPlugin + +fileprivate let macros: [String : Macro.Type] = [ + "Database": DatabaseMacro.self +] + +final class DatabaseMacroTests: XCTestCase { + + func test_table() { + + assertMacroExpansion( + #""" + @Database + struct MyDatabase { + let user: String + } + """#, + expandedSource: #""" + struct MyDatabase { + @Table + let user: String + } + """#, + macros: macros + ) + + } + + func test_member() { + + assertMacroExpansion( + #""" + @Database + struct MyDatabase { + let user: String + } + """#, + expandedSource: #""" + struct MyDatabase { + @Table + let user: String + } + """#, + macros: macros + ) + + + } + +} + +#endif From e3aabc3fa7c61c63d672a2405df199a901f56671 Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 30 Aug 2023 04:04:24 +0900 Subject: [PATCH 03/16] :evergreen_tree: Update --- Package.swift | 12 ++++++ Sources/VergeMacrosPlugin/DatabaseMacro.swift | 43 ++++--------------- .../NormalizedStorageType.swift | 2 +- Sources/VergeNormalization/Table.swift | 31 +++++++------ .../VergeNormalization+Macros.swift | 8 ++-- .../VergeNormalization.swift | 1 + .../VergeNormalizationDerived.swift} | 0 Tests/VergeNormalizationTests/Entities.swift | 36 ++++++++++++++++ Tests/VergeNormalizationTests/Source.swift | 18 ++++++++ .../VergeNormalizationTests.swift | 8 ++++ 10 files changed, 105 insertions(+), 54 deletions(-) rename Sources/{VergeNormalization/ModifyingTable.swift => VergeNormalizationDerived/VergeNormalizationDerived.swift} (100%) create mode 100644 Tests/VergeNormalizationTests/Entities.swift create mode 100644 Tests/VergeNormalizationTests/Source.swift create mode 100644 Tests/VergeNormalizationTests/VergeNormalizationTests.swift diff --git a/Package.swift b/Package.swift index 5dab84e09a..5def545147 100644 --- a/Package.swift +++ b/Package.swift @@ -62,8 +62,16 @@ let package = Package( ), .target( name: "VergeNormalization", + dependencies: [ + "VergeMacros", + .product(name: "HashTreeCollections", package: "swift-collections"), + ] + ), + .target( + name: "VergeNormalizationDerived", dependencies: [ "Verge", + "VergeNormalization", .product(name: "HashTreeCollections", package: "swift-collections"), ] ), @@ -87,6 +95,10 @@ let package = Package( name: "VergeClassicTests", dependencies: ["VergeClassic"] ), + .testTarget( + name: "VergeNormalizationTests", + dependencies: ["VergeNormalization"] + ), .testTarget( name: "VergeORMTests", dependencies: ["VergeORM"] diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/DatabaseMacro.swift index f5f26ce0ae..3a3f4f98d2 100644 --- a/Sources/VergeMacrosPlugin/DatabaseMacro.swift +++ b/Sources/VergeMacrosPlugin/DatabaseMacro.swift @@ -37,37 +37,6 @@ extension DatabaseMacro: ExtensionMacro { } -//extension DatabaseMacro: PeerMacro { -// public static func expansion( -// of node: SwiftSyntax.AttributeSyntax, -// providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, -// in context: some SwiftSyntaxMacros.MacroExpansionContext -// ) throws -> [SwiftSyntax.DeclSyntax] { -// -// guard let structDecl = declaration.as(StructDeclSyntax.self) else { -// fatalError() -// } -// -// return [ -// """ -//extension \(structDecl.name.trimmed) { -// struct A {} -//} -//""" -// ] -// } -// -//} - -extension DatabaseMacro: DeclarationMacro { - public static func expansion( - of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, - in context: some SwiftSyntaxMacros.MacroExpansionContext - ) throws -> [SwiftSyntax.DeclSyntax] { - return [] - } -} - /// Add @Table extension DatabaseMacro: MemberAttributeMacro { @@ -78,6 +47,10 @@ extension DatabaseMacro: MemberAttributeMacro { in context: some SwiftSyntaxMacros.MacroExpansionContext ) throws -> [SwiftSyntax.AttributeSyntax] { + /** + Add macro attribute to member only Table type. + */ + if let variableDecl = member.as(VariableDeclSyntax.self) { let isGenerated = variableDecl @@ -91,12 +64,12 @@ extension DatabaseMacro: MemberAttributeMacro { return [] } - // if isComputedProperty(from: variableDecl) { - // return [] - // } +// if isComputedProperty(from: variableDecl) { +// return [] +// } return [ - "@Table" + "@TableAccessor" ] } diff --git a/Sources/VergeNormalization/NormalizedStorageType.swift b/Sources/VergeNormalization/NormalizedStorageType.swift index 2b954b8270..491d85f30c 100644 --- a/Sources/VergeNormalization/NormalizedStorageType.swift +++ b/Sources/VergeNormalization/NormalizedStorageType.swift @@ -18,7 +18,7 @@ extension NormalizedStorageType { return context } - public consuming func commitBatchUpdates(transaction: consuming ModifyingTransaction) { + public mutating func commitBatchUpdates(transaction: consuming ModifyingTransaction) { // middlewareAfter do { diff --git a/Sources/VergeNormalization/Table.swift b/Sources/VergeNormalization/Table.swift index 24dfe09190..7bf13f9f96 100644 --- a/Sources/VergeNormalization/Table.swift +++ b/Sources/VergeNormalization/Table.swift @@ -15,10 +15,7 @@ public struct Table: Equatable { _read { yield storage.isEmpty } } - public let identifier: String - - init(identifier: String, entities: TreeDictionary = .init()) { - self.identifier = identifier + public init(entities: TreeDictionary = .init()) { self.storage = entities } @@ -36,7 +33,7 @@ public struct Table: Equatable { Finds an entity by the identifier of the entity. - Returns: An entity that found by identifier. Nil if the table does not have that entity. */ - public borrowing func find(by id: Entity.EntityID) -> Entity? { + public borrowing func find(by id: consuming Entity.EntityID) -> Entity? { return storage[id] } @@ -45,7 +42,7 @@ public struct Table: Equatable { /// /// if ids contains same id, result also contains same element. /// - Parameter ids: sequence of Entity.ID - public borrowing func find(in ids: S) -> [Entity] where S.Element == Entity.EntityID { + public borrowing func find(in ids: consuming S) -> [Entity] where S.Element == Entity.EntityID { return ids.reduce(into: [Entity]()) { (buf, id) in guard let entity = storage[id] else { return } @@ -60,8 +57,8 @@ public struct Table: Equatable { */ @discardableResult @inline(__always) - public consuming func updateExists( - id: Entity.EntityID, + public mutating func updateExists( + id: consuming Entity.EntityID, update: (inout Entity) throws -> Void ) throws -> Entity { @@ -84,8 +81,8 @@ public struct Table: Equatable { - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) */ @discardableResult - public consuming func updateIfExists( - id: Entity.EntityID, + public mutating func updateIfExists( + id: consuming Entity.EntityID, update: (inout Entity) throws -> Void ) rethrows -> Entity? { try? updateExists(id: id, update: update) @@ -95,20 +92,22 @@ public struct Table: Equatable { Inserts an entity */ @discardableResult - public consuming func insert(_ entity: Entity) -> InsertionResult { + public mutating func insert(_ entity: consuming Entity) -> InsertionResult { + + let copied = copy entity - storage[entity.entityID] = entity + storage[entity.entityID] = copied updatedMarker.increment() - return .init(entity: entity) + return .init(entity: copied) } /** Inserts a collection of the entity. */ @discardableResult - public consuming func insert(_ addingEntities: S) -> [InsertionResult] where S.Element == Entity { + public mutating func insert(_ addingEntities: consuming S) -> [InsertionResult] where S.Element == Entity { let results = addingEntities.map { entity -> InsertionResult in storage[entity.entityID] = entity @@ -145,5 +144,9 @@ extension Table { entity.entityID } public let entity: Entity + + init(entity: consuming Entity) { + self.entity = entity + } } } diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index d470d89f2d..999fc2c93a 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -2,10 +2,10 @@ @attached(member, names: arbitrary) @attached(memberAttribute) @attached(extension, conformances: NormalizedStorageType, Equatable, names: named(Context), named(BBB)) -public macro Database() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseMacro") +public macro NormalizedStorage() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseMacro") @attached(accessor) -public macro Table() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseTableMacro") +public macro TableAccessor() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseTableMacro") #if DEBUG @@ -16,14 +16,14 @@ struct A: EntityType { } } -@Database +@NormalizedStorage struct MyDatabase { var user: Table } private func play() { - var db = MyDatabase(_$user: .init(identifier: "")) + var db = MyDatabase(_$user: .init()) db.performBatchUpdates { t in t.modifying.user.insert(.init()) diff --git a/Sources/VergeNormalization/VergeNormalization.swift b/Sources/VergeNormalization/VergeNormalization.swift index 8b13789179..2d46c51b65 100644 --- a/Sources/VergeNormalization/VergeNormalization.swift +++ b/Sources/VergeNormalization/VergeNormalization.swift @@ -1 +1,2 @@ +@_exported import HashTreeCollections diff --git a/Sources/VergeNormalization/ModifyingTable.swift b/Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift similarity index 100% rename from Sources/VergeNormalization/ModifyingTable.swift rename to Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift diff --git a/Tests/VergeNormalizationTests/Entities.swift b/Tests/VergeNormalizationTests/Entities.swift new file mode 100644 index 0000000000..f62fd4b38a --- /dev/null +++ b/Tests/VergeNormalizationTests/Entities.swift @@ -0,0 +1,36 @@ +import VergeNormalization + +struct Book: EntityType, Hashable { + + typealias EntityIDRawType = String + + var entityID: EntityID { + .init(rawID) + } + + let rawID: String + let authorID: Author.EntityID + var name: String = "initial" +} + +struct Author: EntityType { + + typealias EntityIDRawType = String + + var entityID: EntityID { + .init(rawID) + } + + let rawID: String + var name: String = "" + + static let anonymous: Author = .init(rawID: "anonymous") +} + +@NormalizedStorage +struct MyStorage { + + var book: Table = .init() + var author: Table = .init() + +} diff --git a/Tests/VergeNormalizationTests/Source.swift b/Tests/VergeNormalizationTests/Source.swift new file mode 100644 index 0000000000..1872ca50d7 --- /dev/null +++ b/Tests/VergeNormalizationTests/Source.swift @@ -0,0 +1,18 @@ +import VergeNormalization +import XCTest + +final class Tests: XCTestCase { + + func test_insert() { + + var storage = MyStorage() + + storage.performBatchUpdates { t in + t.modifying.author.insert(.init(rawID: "M")) + } + + XCTAssertEqual(storage.author.count, 1) + + } + +} diff --git a/Tests/VergeNormalizationTests/VergeNormalizationTests.swift b/Tests/VergeNormalizationTests/VergeNormalizationTests.swift new file mode 100644 index 0000000000..b66746f472 --- /dev/null +++ b/Tests/VergeNormalizationTests/VergeNormalizationTests.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Muukii on 2023/08/30. +// + +import Foundation From 4abc520bfe7795b333b3c525a5cd442131e9123f Mon Sep 17 00:00:00 2001 From: Muukii Date: Thu, 31 Aug 2023 03:11:03 +0900 Subject: [PATCH 04/16] :evergreen_tree: Update --- Package.swift | 1 + Sources/VergeMacrosPlugin/DatabaseMacro.swift | 47 +++++++++++++ Sources/VergeNormalization/Selector.swift | 46 ++++++++++++ Sources/VergeNormalization/Table.swift | 2 + .../VergeNormalization+Macros.swift | 8 ++- .../DispatcherType+.swift | 54 ++++++++++++++ .../EntityType+Typealias.swift | 7 ++ .../EntityWrapper.swift | 23 ++++++ .../NonNullEntityWrapper.swift | 36 ++++++++++ .../VergeNormalizationDerived.swift | 10 +-- Sources/VergeORM/Derived+ORM.swift | 70 +------------------ .../VergeMacrosTests/DatabaseMacroTests.swift | 9 ++- 12 files changed, 230 insertions(+), 83 deletions(-) create mode 100644 Sources/VergeNormalization/Selector.swift create mode 100644 Sources/VergeNormalizationDerived/DispatcherType+.swift create mode 100644 Sources/VergeNormalizationDerived/EntityType+Typealias.swift create mode 100644 Sources/VergeNormalizationDerived/EntityWrapper.swift create mode 100644 Sources/VergeNormalizationDerived/NonNullEntityWrapper.swift diff --git a/Package.swift b/Package.swift index 5def545147..60624ba2ca 100644 --- a/Package.swift +++ b/Package.swift @@ -79,6 +79,7 @@ let package = Package( name: "VergeORM", dependencies: [ "VergeNormalization", + "VergeNormalizationDerived", "Verge", .product(name: "HashTreeCollections", package: "swift-collections"), ] diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/DatabaseMacro.swift index 3a3f4f98d2..a7cb8f3a9e 100644 --- a/Sources/VergeMacrosPlugin/DatabaseMacro.swift +++ b/Sources/VergeMacrosPlugin/DatabaseMacro.swift @@ -19,7 +19,54 @@ extension DatabaseMacro: ExtensionMacro { fatalError() } + let tableMembers = structDecl.memberBlock.members + .compactMap { + $0.decl.as(VariableDeclSyntax.self) + } + .filter { + guard $0.bindings.count == 1 else { + return false + } + guard $0.bindings.first!.typeAnnotation != nil else { + return false + } + return true + } + .filter { + $0.attributes.contains { + switch $0 { + case .attribute(let attribute): + return attribute.attributeName.description == "TableAccessor" + case .ifConfigDecl: + return false + } + } + } + + let decls = tableMembers.map { member in + """ + struct \(member.bindings.first!.pattern.trimmed): TableSelector { + typealias _Table = \(member.bindings.first!.typeAnnotation!.type.description) + typealias Entity = _Table.Entity + typealias Storage = \(structDecl.name.trimmed) + + func select(storage: Storage) -> Table { + storage.\(member.bindings.first!.pattern.trimmed) + } + + } + """ + } + + let ext = (""" + extension \(structDecl.name.trimmed) { + \(raw: decls.joined(separator: "\n")) + } + """ as DeclSyntax).cast(ExtensionDeclSyntax.self) + + return [ + ext, (""" extension \(structDecl.name.trimmed): NormalizedStorageType {} """ as DeclSyntax).cast(ExtensionDeclSyntax.self), diff --git a/Sources/VergeNormalization/Selector.swift b/Sources/VergeNormalization/Selector.swift new file mode 100644 index 0000000000..98377d8120 --- /dev/null +++ b/Sources/VergeNormalization/Selector.swift @@ -0,0 +1,46 @@ +public protocol TableSelector { + associatedtype Entity: EntityType + associatedtype Storage: NormalizedStorageType + func select(storage: Storage) -> Table +} + +public protocol StorageSelector { + associatedtype Source + associatedtype Storage: NormalizedStorageType + + func select(source: Source) -> Storage +} + +extension StorageSelector { + + public func append<_TableSelector: TableSelector>( + _ tableSelector: consuming _TableSelector + ) + -> AbsoluteTableSelector + { + AbsoluteTableSelector(storage: self, table: tableSelector) + } + +} + +public struct AbsoluteTableSelector< + _StorageSelector: StorageSelector, + _TableSelector: TableSelector +> where _StorageSelector.Storage == _TableSelector.Storage { + + public let storage: _StorageSelector + public let table: _TableSelector + + public init( + storage: consuming _StorageSelector, + table: consuming _TableSelector + ) { + self.storage = storage + self.table = table + } + + public func select(source: _StorageSelector.Source) -> Table<_TableSelector.Entity> { + table.select(storage: storage.select(source: source)) + } + +} diff --git a/Sources/VergeNormalization/Table.swift b/Sources/VergeNormalization/Table.swift index 7bf13f9f96..1e2a790a75 100644 --- a/Sources/VergeNormalization/Table.swift +++ b/Sources/VergeNormalization/Table.swift @@ -2,6 +2,8 @@ import struct HashTreeCollections.TreeDictionary public struct Table: Equatable { + public typealias Entity = Entity + private var storage: TreeDictionary private(set) var updatedMarker = NonAtomicCounter() diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index 999fc2c93a..859b624450 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -1,12 +1,13 @@ @attached(member, names: arbitrary) -@attached(memberAttribute) -@attached(extension, conformances: NormalizedStorageType, Equatable, names: named(Context), named(BBB)) +//@attached(memberAttribute) +@attached(extension, conformances: NormalizedStorageType, Equatable, names: named(Context), named(BBB), arbitrary) public macro NormalizedStorage() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseMacro") @attached(accessor) public macro TableAccessor() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseTableMacro") + #if DEBUG struct A: EntityType { @@ -18,12 +19,13 @@ struct A: EntityType { @NormalizedStorage struct MyDatabase { + @TableAccessor var user: Table } private func play() { - var db = MyDatabase(_$user: .init()) + var db = MyDatabase.init() db.performBatchUpdates { t in t.modifying.user.insert(.init()) diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift new file mode 100644 index 0000000000..47796b87f2 --- /dev/null +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -0,0 +1,54 @@ +extension DispatcherType { + + public func derivedEntity< + _StorageSelector: StorageSelector, + _TableSelector: TableSelector + >( + selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector> + ) -> Derived> + where + _StorageSelector.Storage == _TableSelector.Storage, + _StorageSelector.Source == Changes + { + + derived(SingleEntityPipeline(selector: selector), queue: .passthrough) + + } + +} + +private struct SingleEntityPipeline< + _StorageSelector: StorageSelector, + _TableSelector: TableSelector +>: PipelineType +where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Source: ChangesType { + + typealias Entity = _TableSelector.Entity + typealias Input = _StorageSelector.Source + typealias Output = EntityWrapper + + private let selector: AbsoluteTableSelector<_StorageSelector, _TableSelector> + private let entityID: _TableSelector.Entity.EntityID + + init( + targetIdentifier: _TableSelector.Entity.EntityID, + selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector> + ) { + self.entityID = targetIdentifier + self.selector = selector + } + + func yield(_ input: consuming Input) -> EntityWrapper { + + let result = selector.select(source: input) + .find(by: entityID) + + return .init(id: entityID, entity: consume result) + + } + + func yieldContinuously(_ input: Input) -> Verge.ContinuousResult> { + fatalError() + } + +} diff --git a/Sources/VergeNormalizationDerived/EntityType+Typealias.swift b/Sources/VergeNormalizationDerived/EntityType+Typealias.swift new file mode 100644 index 0000000000..2cd76507b5 --- /dev/null +++ b/Sources/VergeNormalizationDerived/EntityType+Typealias.swift @@ -0,0 +1,7 @@ + +extension EntityType { + + public typealias Derived = Verge.Derived> + public typealias NonNullDerived = Verge.Derived> + +} diff --git a/Sources/VergeNormalizationDerived/EntityWrapper.swift b/Sources/VergeNormalizationDerived/EntityWrapper.swift new file mode 100644 index 0000000000..4d6474ee32 --- /dev/null +++ b/Sources/VergeNormalizationDerived/EntityWrapper.swift @@ -0,0 +1,23 @@ +import VergeNormalization + +/// A value that wraps an entity and results of fetching. +public struct EntityWrapper: Sendable { + + public private(set) var wrapped: Entity? + public let id: Entity.EntityID + + public init(id: Entity.EntityID, entity: Entity?) { + self.id = id + self.wrapped = entity + } + +} + +extension EntityWrapper: Equatable where Entity: Equatable { + +} + +extension EntityWrapper: Hashable where Entity: Hashable { + +} + diff --git a/Sources/VergeNormalizationDerived/NonNullEntityWrapper.swift b/Sources/VergeNormalizationDerived/NonNullEntityWrapper.swift new file mode 100644 index 0000000000..3fbf4678e5 --- /dev/null +++ b/Sources/VergeNormalizationDerived/NonNullEntityWrapper.swift @@ -0,0 +1,36 @@ +import VergeNormalization + +/// A value that wraps an entity and results of fetching. +@dynamicMemberLookup +public struct NonNullEntityWrapper { + + /// An entity value + public private(set) var wrapped: Entity + + /// An identifier + public let id: Entity.EntityID + + @available(*, deprecated, renamed: "isFallBack") + public var isUsingFallback: Bool { + isFallBack + } + + /// A boolean value that indicates whether the wrapped entity is last value and has been removed from source store. + public let isFallBack: Bool + + public init(entity: Entity, isFallBack: Bool) { + self.id = entity.entityID + self.wrapped = entity + self.isFallBack = isFallBack + } + + public subscript(dynamicMember keyPath: KeyPath) -> Property { + wrapped[keyPath: keyPath] + } + +} + +extension NonNullEntityWrapper: Equatable where Entity: Equatable {} + +extension NonNullEntityWrapper: Hashable where Entity: Hashable {} + diff --git a/Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift b/Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift index b66746f472..50c8aee2d7 100644 --- a/Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift +++ b/Sources/VergeNormalizationDerived/VergeNormalizationDerived.swift @@ -1,8 +1,2 @@ -// -// File.swift -// -// -// Created by Muukii on 2023/08/30. -// - -import Foundation +@_exported import Verge +@_exported import VergeNormalization diff --git a/Sources/VergeORM/Derived+ORM.swift b/Sources/VergeORM/Derived+ORM.swift index e8f843eebb..bc500f02a2 100644 --- a/Sources/VergeORM/Derived+ORM.swift +++ b/Sources/VergeORM/Derived+ORM.swift @@ -21,81 +21,13 @@ import Foundation -#if !COCOAPODS import Verge -#endif +import VergeNormalizationDerived public enum VergeORMError: Swift.Error { case notFoundEntityFromDatabase } -extension EntityType { - -#if COCOAPODS - public typealias Derived = Verge.Derived> - public typealias NonNullDerived = Verge.Derived> -#else - public typealias Derived = Verge.Derived> - public typealias NonNullDerived = Verge.Derived> -#endif - -} - -/// A value that wraps an entity and results of fetching. -public struct EntityWrapper: Sendable { - - public private(set) var wrapped: Entity? - public let id: Entity.EntityID - - public init(id: Entity.EntityID, entity: Entity?) { - self.id = id - self.wrapped = entity - } - -} - -extension EntityWrapper: Equatable where Entity: Equatable { - -} - -extension EntityWrapper: Hashable where Entity: Hashable { - -} - -/// A value that wraps an entity and results of fetching. -@dynamicMemberLookup -public struct NonNullEntityWrapper { - - /// An entity value - public private(set) var wrapped: Entity - - /// An identifier - public let id: Entity.EntityID - - @available(*, deprecated, renamed: "isFallBack") - public var isUsingFallback: Bool { - isFallBack - } - - /// A boolean value that indicates whether the wrapped entity is last value and has been removed from source store. - public let isFallBack: Bool - - public init(entity: Entity, isFallBack: Bool) { - self.id = entity.entityID - self.wrapped = entity - self.isFallBack = isFallBack - } - - public subscript(dynamicMember keyPath: KeyPath) -> Property { - wrapped[keyPath: keyPath] - } - -} - -extension NonNullEntityWrapper: Equatable where Entity: Equatable {} - -extension NonNullEntityWrapper: Hashable where Entity: Hashable {} - private final class DerivedCacheKey: NSObject { diff --git a/Tests/VergeMacrosTests/DatabaseMacroTests.swift b/Tests/VergeMacrosTests/DatabaseMacroTests.swift index f37b8c00af..9aa6c945f4 100644 --- a/Tests/VergeMacrosTests/DatabaseMacroTests.swift +++ b/Tests/VergeMacrosTests/DatabaseMacroTests.swift @@ -6,7 +6,7 @@ import XCTest import VergeMacrosPlugin fileprivate let macros: [String : Macro.Type] = [ - "Database": DatabaseMacro.self + "NormalizedStorage": DatabaseMacro.self ] final class DatabaseMacroTests: XCTestCase { @@ -15,8 +15,11 @@ final class DatabaseMacroTests: XCTestCase { assertMacroExpansion( #""" - @Database + @NormalizedStorage struct MyDatabase { + @TableAccessor + let user: String + @TableAccessor(hoge) let user: String } """#, @@ -35,7 +38,7 @@ final class DatabaseMacroTests: XCTestCase { assertMacroExpansion( #""" - @Database + @NormalizedStorage struct MyDatabase { let user: String } From db9e63dfc94685b83270cf028d55529172de94b7 Mon Sep 17 00:00:00 2001 From: Muukii Date: Thu, 31 Aug 2023 03:31:43 +0900 Subject: [PATCH 05/16] :zap: WIP --- Package.swift | 3 + Sources/Verge/Verge.swift | 1 + .../Comparator.swift} | 1 + Sources/VergeMacrosPlugin/DatabaseMacro.swift | 27 +++++--- Sources/VergeNormalization/Comparison.swift | 62 +++++++++++++++++++ .../NormalizedStorageType.swift | 2 +- .../VergeNormalization+Macros.swift | 6 +- .../DispatcherType+.swift | 41 +++++++++++- 8 files changed, 127 insertions(+), 16 deletions(-) rename Sources/{Verge/Store/Comparer.swift => VergeComparator/Comparator.swift} (99%) create mode 100644 Sources/VergeNormalization/Comparison.swift diff --git a/Package.swift b/Package.swift index 60624ba2ca..dd0a3c28ea 100644 --- a/Package.swift +++ b/Package.swift @@ -45,10 +45,12 @@ let package = Package( .target(name: "VergeMacros", dependencies: ["VergeMacrosPlugin"]), .target(name: "VergeTiny", dependencies: []), + .target(name: "VergeComparator"), .target( name: "Verge", dependencies: [ "VergeMacros", + "VergeComparator", .product(name: "Atomics", package: "swift-atomics"), .product(name: "DequeModule", package: "swift-collections"), .product(name: "ConcurrencyTaskManager", package: "swift-concurrency-task-manager"), @@ -64,6 +66,7 @@ let package = Package( name: "VergeNormalization", dependencies: [ "VergeMacros", + "VergeComparator", .product(name: "HashTreeCollections", package: "swift-collections"), ] ), diff --git a/Sources/Verge/Verge.swift b/Sources/Verge/Verge.swift index 975e1ffe0c..0c70f8cc2c 100644 --- a/Sources/Verge/Verge.swift +++ b/Sources/Verge/Verge.swift @@ -1,3 +1,4 @@ @_exported import ConcurrencyTaskManager @_exported import Combine +@_exported import VergeComparator diff --git a/Sources/Verge/Store/Comparer.swift b/Sources/VergeComparator/Comparator.swift similarity index 99% rename from Sources/Verge/Store/Comparer.swift rename to Sources/VergeComparator/Comparator.swift index 57903fe9a2..e351a0a783 100644 --- a/Sources/Verge/Store/Comparer.swift +++ b/Sources/VergeComparator/Comparator.swift @@ -22,6 +22,7 @@ import Foundation import os.log +// TODO: will rename as Comparator public protocol Comparison: Sendable { associatedtype Input diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/DatabaseMacro.swift index a7cb8f3a9e..f2348ebbae 100644 --- a/Sources/VergeMacrosPlugin/DatabaseMacro.swift +++ b/Sources/VergeMacrosPlugin/DatabaseMacro.swift @@ -216,6 +216,13 @@ public struct DatabaseTableMacro: Macro { } +extension DatabaseTableMacro: PeerMacro { + public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + return [] + } + +} + extension DatabaseTableMacro: AccessorMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, @@ -230,16 +237,16 @@ extension DatabaseTableMacro: AccessorMacro { let id = identifier(from: declaration.cast(VariableDeclSyntax.self)) return [ - """ - get { - _$\(id) - } - """, - """ - set { - _$\(id) = newValue - } - """ , +// """ +// get { +// _$\(id) +// } +// """, +// """ +// set { +// _$\(id) = newValue +// } +// """ , ] } diff --git a/Sources/VergeNormalization/Comparison.swift b/Sources/VergeNormalization/Comparison.swift new file mode 100644 index 0000000000..81708d8a65 --- /dev/null +++ b/Sources/VergeNormalization/Comparison.swift @@ -0,0 +1,62 @@ +import VergeComparator + +public enum NormalizedStorageComparisons { + + /// True indicates database is not changed + public struct StorageComparison: Comparison { + public typealias Input = Storage + + public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { + + lhs == rhs + +// (lhs._backingStorage.entityUpdatedMarker, lhs._backingStorage.indexUpdatedMarker) == (rhs._backingStorage.entityUpdatedMarker, rhs._backingStorage.indexUpdatedMarker) + } + } + + /// Returns true if the table of the entity in database has no changes. + /// + /// - Complexity: O(1) + public struct TableComparison: Comparison { + + public typealias Input = Storage + + public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { +// lhs._backingStorage.entityBackingStorage.table(Entity.self).updatedMarker == rhs._backingStorage.entityBackingStorage.table(Entity.self).updatedMarker + fatalError() + } + + } + + /// Returns true if the updates result does not contain the entity. + public struct UpdateComparison: Comparison { + + public typealias Input = Storage + + + public let entityID: Entity.EntityID + + public init(entityID: Entity.EntityID) { + self.entityID = entityID + } + + public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { + + fatalError() + +// guard let result = rhs._backingStorage.lastUpdatesResult else { +// return false +// } +// guard !result.wasUpdated(entityID) else { +// return false +// } +// guard !result.wasDeleted(entityID) else { +// return false +// } +// return true + + } + } + +} + diff --git a/Sources/VergeNormalization/NormalizedStorageType.swift b/Sources/VergeNormalization/NormalizedStorageType.swift index 491d85f30c..86b591bd59 100644 --- a/Sources/VergeNormalization/NormalizedStorageType.swift +++ b/Sources/VergeNormalization/NormalizedStorageType.swift @@ -1,5 +1,5 @@ -public protocol NormalizedStorageType { +public protocol NormalizedStorageType: Equatable { func finalizeTransaction(transaction: inout ModifyingTransaction) diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index 859b624450..735744ec10 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -1,10 +1,10 @@ -@attached(member, names: arbitrary) +//@attached(member, names: arbitrary) //@attached(memberAttribute) @attached(extension, conformances: NormalizedStorageType, Equatable, names: named(Context), named(BBB), arbitrary) public macro NormalizedStorage() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseMacro") -@attached(accessor) +@attached(peer) public macro TableAccessor() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseTableMacro") @@ -25,7 +25,7 @@ struct MyDatabase { private func play() { - var db = MyDatabase.init() + var db = MyDatabase.init(user: .init()) db.performBatchUpdates { t in t.modifying.user.insert(.init()) diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift index 47796b87f2..8481fe0112 100644 --- a/Sources/VergeNormalizationDerived/DispatcherType+.swift +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -4,17 +4,49 @@ extension DispatcherType { _StorageSelector: StorageSelector, _TableSelector: TableSelector >( - selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector> + selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, + entityID: _TableSelector.Entity.EntityID ) -> Derived> where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Source == Changes { - derived(SingleEntityPipeline(selector: selector), queue: .passthrough) + // TODO: caching + + return derived( + SingleEntityPipeline( + targetIdentifier: entityID, + selector: selector + ), + queue: .passthrough + ) } + public func derivedEntity2< + _StorageSelector: StorageSelector, + _TableSelector: TableSelector + >( + selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, + entityID: _TableSelector.Entity.EntityID + ) -> Derived> + where + _StorageSelector.Storage == _TableSelector.Storage, + _StorageSelector.Source == Changes + { + + // TODO: caching + + return derived( + SingleEntityPipeline( + targetIdentifier: entityID, + selector: selector + ), + queue: .passthrough + ) + + } } private struct SingleEntityPipeline< @@ -48,6 +80,11 @@ where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Sourc } func yieldContinuously(_ input: Input) -> Verge.ContinuousResult> { + + guard let previous = input.previous else { + return .new(yield(input)) + } + fatalError() } From ce0c4dc72444947a85aed2d395e4105cd35a27de Mon Sep 17 00:00:00 2001 From: Muukii Date: Thu, 31 Aug 2023 05:01:02 +0900 Subject: [PATCH 06/16] :evergreen_tree: Update --- Sources/VergeMacrosPlugin/DatabaseMacro.swift | 35 +++++++++++++++---- Sources/VergeNormalization/Table.swift | 2 +- .../VergeNormalization+Macros.swift | 16 +++++++++ Tests/VergeNormalizationTests/Entities.swift | 2 ++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/DatabaseMacro.swift index f2348ebbae..a260f82516 100644 --- a/Sources/VergeMacrosPlugin/DatabaseMacro.swift +++ b/Sources/VergeMacrosPlugin/DatabaseMacro.swift @@ -43,7 +43,29 @@ extension DatabaseMacro: ExtensionMacro { } } - let decls = tableMembers.map { member in + let comparator = { + + let markerComparators = tableMembers.map { member in + member.bindings.first!.pattern.trimmed + } + .map { name in + "guard lhs.\(name).updatedMarker == rhs.\(name).updatedMarker else { return lhs == rhs }" + } + + return (""" + extension \(structDecl.name.trimmed) { + static func compare(lhs: Self, rhs: Self) -> Bool { + \(raw: markerComparators.joined(separator: "\n")) + return true + } + } + """ as DeclSyntax).cast(ExtensionDeclSyntax.self) + + }() + + let selectors = { + + let decls = tableMembers.map { member in """ struct \(member.bindings.first!.pattern.trimmed): TableSelector { typealias _Table = \(member.bindings.first!.typeAnnotation!.type.description) @@ -56,17 +78,18 @@ extension DatabaseMacro: ExtensionMacro { } """ - } + } - let ext = (""" + return (""" extension \(structDecl.name.trimmed) { \(raw: decls.joined(separator: "\n")) } """ as DeclSyntax).cast(ExtensionDeclSyntax.self) - + }() return [ - ext, + comparator, + selectors, (""" extension \(structDecl.name.trimmed): NormalizedStorageType {} """ as DeclSyntax).cast(ExtensionDeclSyntax.self), @@ -220,7 +243,7 @@ extension DatabaseTableMacro: PeerMacro { public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { return [] } - + } extension DatabaseTableMacro: AccessorMacro { diff --git a/Sources/VergeNormalization/Table.swift b/Sources/VergeNormalization/Table.swift index 1e2a790a75..e6befa7e98 100644 --- a/Sources/VergeNormalization/Table.swift +++ b/Sources/VergeNormalization/Table.swift @@ -5,7 +5,7 @@ public struct Table: Equatable { public typealias Entity = Entity private var storage: TreeDictionary - private(set) var updatedMarker = NonAtomicCounter() + public private(set) var updatedMarker = NonAtomicCounter() /// The number of entities in table public var count: Int { diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index 735744ec10..34fb8402a6 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -21,6 +21,22 @@ struct A: EntityType { struct MyDatabase { @TableAccessor var user: Table + + @TableAccessor + var user2: Table = .init() + + @TableAccessor + var user3: Table = .init() +} + +extension MyDatabase { + + static func c(lhs: Self, rhs: Self) -> Bool { + + lhs.user.updatedMarker == rhs.user.updatedMarker + + } + } private func play() { diff --git a/Tests/VergeNormalizationTests/Entities.swift b/Tests/VergeNormalizationTests/Entities.swift index f62fd4b38a..074192939d 100644 --- a/Tests/VergeNormalizationTests/Entities.swift +++ b/Tests/VergeNormalizationTests/Entities.swift @@ -30,7 +30,9 @@ struct Author: EntityType { @NormalizedStorage struct MyStorage { + @TableAccessor var book: Table = .init() + @TableAccessor var author: Table = .init() } From 5ebd1a1fc6660f78c072abaf16b9db40d7711ca1 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sat, 2 Sep 2023 15:10:55 +0900 Subject: [PATCH 07/16] :zap: WIP --- Package.swift | 4 + Sources/VergeMacrosPlugin/IndexMacro.swift | 7 + ...cro.swift => NormalizedStorageMacro.swift} | 72 +---- Sources/VergeMacrosPlugin/Plugin.swift | 4 +- Sources/VergeMacrosPlugin/TableMacro.swift | 43 +++ Sources/VergeNormalization/Comparison.swift | 76 +++--- .../VergeNormalization/Indexes/Indexes.swift | 41 +++ .../VergeNormalization/NonAtomicCounter.swift | 2 +- .../NormalizedStorageType.swift | 1 + Sources/VergeNormalization/Selector.swift | 22 +- Sources/VergeNormalization/Table.swift | 154 ----------- .../VergeNormalization/Tables/TableType.swift | 50 ++++ .../Tables/Tables.Hash.swift | 145 ++++++++++ .../VergeNormalization/Tables/Tables.swift | 4 + .../VergeNormalization+Macros.swift | 18 +- .../VergeNormalization.swift | 1 - .../DispatcherType+.swift | 26 +- .../VergeMacrosTests/DatabaseMacroTests.swift | 2 +- .../DemoState.swift | 59 ++++ .../VergeNormalizationDerivedTests.swift | 42 +++ Tests/VergeNormalizationTests/Entities.swift | 8 +- Tests/VergeNormalizationTests/Source.swift | 257 ++++++++++++++++++ 22 files changed, 755 insertions(+), 283 deletions(-) create mode 100644 Sources/VergeMacrosPlugin/IndexMacro.swift rename Sources/VergeMacrosPlugin/{DatabaseMacro.swift => NormalizedStorageMacro.swift} (76%) create mode 100644 Sources/VergeMacrosPlugin/TableMacro.swift create mode 100644 Sources/VergeNormalization/Indexes/Indexes.swift delete mode 100644 Sources/VergeNormalization/Table.swift create mode 100644 Sources/VergeNormalization/Tables/TableType.swift create mode 100644 Sources/VergeNormalization/Tables/Tables.Hash.swift create mode 100644 Sources/VergeNormalization/Tables/Tables.swift create mode 100644 Tests/VergeNormalizationDerivedTests/DemoState.swift create mode 100644 Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift diff --git a/Package.swift b/Package.swift index dd0a3c28ea..c2314cb2a4 100644 --- a/Package.swift +++ b/Package.swift @@ -103,6 +103,10 @@ let package = Package( name: "VergeNormalizationTests", dependencies: ["VergeNormalization"] ), + .testTarget( + name: "VergeNormalizationDerivedTests", + dependencies: ["VergeNormalizationDerived"] + ), .testTarget( name: "VergeORMTests", dependencies: ["VergeORM"] diff --git a/Sources/VergeMacrosPlugin/IndexMacro.swift b/Sources/VergeMacrosPlugin/IndexMacro.swift new file mode 100644 index 0000000000..41754f8d3e --- /dev/null +++ b/Sources/VergeMacrosPlugin/IndexMacro.swift @@ -0,0 +1,7 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct DatabaseIndexMacro: Macro { + +} diff --git a/Sources/VergeMacrosPlugin/DatabaseMacro.swift b/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift similarity index 76% rename from Sources/VergeMacrosPlugin/DatabaseMacro.swift rename to Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift index a260f82516..e16eaf9e0a 100644 --- a/Sources/VergeMacrosPlugin/DatabaseMacro.swift +++ b/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift @@ -2,11 +2,11 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct DatabaseMacro: Macro { +public struct NormalizedStorageMacro: Macro { } -extension DatabaseMacro: ExtensionMacro { +extension NormalizedStorageMacro: ExtensionMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, @@ -36,7 +36,7 @@ extension DatabaseMacro: ExtensionMacro { $0.attributes.contains { switch $0 { case .attribute(let attribute): - return attribute.attributeName.description == "TableAccessor" + return attribute.attributeName.description == "Table" case .ifConfigDecl: return false } @@ -68,14 +68,13 @@ extension DatabaseMacro: ExtensionMacro { let decls = tableMembers.map { member in """ struct \(member.bindings.first!.pattern.trimmed): TableSelector { - typealias _Table = \(member.bindings.first!.typeAnnotation!.type.description) - typealias Entity = _Table.Entity - typealias Storage = \(structDecl.name.trimmed) - - func select(storage: Storage) -> Table { - storage.\(member.bindings.first!.pattern.trimmed) - } + typealias _Table = \(member.bindings.first!.typeAnnotation!.type.description) + typealias Entity = _Table.Entity + typealias Storage = \(structDecl.name.trimmed) + func select(storage: Storage) -> _Table { + storage.\(member.bindings.first!.pattern.trimmed) + } } """ } @@ -107,8 +106,9 @@ extension DatabaseMacro: ExtensionMacro { } +#if false /// Add @Table -extension DatabaseMacro: MemberAttributeMacro { +extension NormalizedStorageMacro: MemberAttributeMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, @@ -139,7 +139,7 @@ extension DatabaseMacro: MemberAttributeMacro { // } return [ - "@TableAccessor" + "@Table" ] } @@ -150,8 +150,10 @@ extension DatabaseMacro: MemberAttributeMacro { } +#endif + /// Add member -extension DatabaseMacro: MemberMacro { +extension NormalizedStorageMacro: MemberMacro { final class RenamingVisitor: SyntaxRewriter { @@ -234,47 +236,3 @@ extension DatabaseMacro: MemberMacro { } } - -public struct DatabaseTableMacro: Macro { - -} - -extension DatabaseTableMacro: PeerMacro { - public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { - return [] - } - -} - -extension DatabaseTableMacro: AccessorMacro { - public static func expansion( - of node: SwiftSyntax.AttributeSyntax, - providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, - in context: some SwiftSyntaxMacros.MacroExpansionContext - ) throws -> [SwiftSyntax.AccessorDeclSyntax] { - - func identifier(from node: VariableDeclSyntax) -> TokenSyntax { - node.bindings.first!.cast(PatternBindingSyntax.self).pattern.cast(IdentifierPatternSyntax.self).identifier - } - - let id = identifier(from: declaration.cast(VariableDeclSyntax.self)) - - return [ -// """ -// get { -// _$\(id) -// } -// """, -// """ -// set { -// _$\(id) = newValue -// } -// """ , - ] - } - -} - -public struct DatabaseIndexMacro: Macro { - -} diff --git a/Sources/VergeMacrosPlugin/Plugin.swift b/Sources/VergeMacrosPlugin/Plugin.swift index 75e51f2924..99e8a6c3c1 100644 --- a/Sources/VergeMacrosPlugin/Plugin.swift +++ b/Sources/VergeMacrosPlugin/Plugin.swift @@ -9,8 +9,8 @@ struct Plugin: CompilerPlugin { DatabaseStateMacro.self, IfChangedMacro.self, - DatabaseMacro.self, - DatabaseTableMacro.self, + NormalizedStorageMacro.self, + TableMacro.self, DatabaseIndexMacro.self ] } diff --git a/Sources/VergeMacrosPlugin/TableMacro.swift b/Sources/VergeMacrosPlugin/TableMacro.swift new file mode 100644 index 0000000000..b66078655a --- /dev/null +++ b/Sources/VergeMacrosPlugin/TableMacro.swift @@ -0,0 +1,43 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct TableMacro: Macro { + +} + +extension TableMacro: PeerMacro { + public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + return [] + } + +} + +extension TableMacro: AccessorMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.AccessorDeclSyntax] { + + func identifier(from node: VariableDeclSyntax) -> TokenSyntax { + node.bindings.first!.cast(PatternBindingSyntax.self).pattern.cast(IdentifierPatternSyntax.self).identifier + } + + let id = identifier(from: declaration.cast(VariableDeclSyntax.self)) + + return [ + // """ + // get { + // _$\(id) + // } + // """, + // """ + // set { + // _$\(id) = newValue + // } + // """ , + ] + } + +} diff --git a/Sources/VergeNormalization/Comparison.swift b/Sources/VergeNormalization/Comparison.swift index 81708d8a65..ddfa613c4f 100644 --- a/Sources/VergeNormalization/Comparison.swift +++ b/Sources/VergeNormalization/Comparison.swift @@ -6,57 +6,57 @@ public enum NormalizedStorageComparisons { public struct StorageComparison: Comparison { public typealias Input = Storage - public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { + public init() {} - lhs == rhs - -// (lhs._backingStorage.entityUpdatedMarker, lhs._backingStorage.indexUpdatedMarker) == (rhs._backingStorage.entityUpdatedMarker, rhs._backingStorage.indexUpdatedMarker) + public func callAsFunction(_ lhs: Input, _ rhs: Input) -> Bool { + Storage.compare(lhs: lhs, rhs: rhs) } } /// Returns true if the table of the entity in database has no changes. - /// - /// - Complexity: O(1) public struct TableComparison: Comparison { - public typealias Input = Storage + public typealias Input = Tables.Hash - public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { -// lhs._backingStorage.entityBackingStorage.table(Entity.self).updatedMarker == rhs._backingStorage.entityBackingStorage.table(Entity.self).updatedMarker - fatalError() + public init() {} + + public func callAsFunction(_ lhs: Input, _ rhs: Input) -> Bool { + guard lhs.updatedMarker == rhs.updatedMarker else { + return lhs == rhs + } + return true } } /// Returns true if the updates result does not contain the entity. - public struct UpdateComparison: Comparison { - - public typealias Input = Storage - - - public let entityID: Entity.EntityID - - public init(entityID: Entity.EntityID) { - self.entityID = entityID - } - - public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { - - fatalError() - -// guard let result = rhs._backingStorage.lastUpdatesResult else { -// return false -// } -// guard !result.wasUpdated(entityID) else { -// return false -// } -// guard !result.wasDeleted(entityID) else { -// return false -// } -// return true - - } - } +// public struct UpdateComparison: Comparison { +// +// public typealias Input = Storage +// +// public let entityID: Entity.EntityID +// +// public init(entityID: Entity.EntityID) { +// self.entityID = entityID +// } +// +// public func callAsFunction(_ lhs: Storage, _ rhs: Storage) -> Bool { +// +// fatalError() +// +//// guard let result = rhs._backingStorage.lastUpdatesResult else { +//// return false +//// } +//// guard !result.wasUpdated(entityID) else { +//// return false +//// } +//// guard !result.wasDeleted(entityID) else { +//// return false +//// } +//// return true +// +// } +// } } diff --git a/Sources/VergeNormalization/Indexes/Indexes.swift b/Sources/VergeNormalization/Indexes/Indexes.swift new file mode 100644 index 0000000000..7c4af0afa8 --- /dev/null +++ b/Sources/VergeNormalization/Indexes/Indexes.swift @@ -0,0 +1,41 @@ + +import HashTreeCollections + +public enum Indexes { + + /// A Indexing store + /// + /// { + /// Grouping-ID : [ + /// - Grouped-ID + /// - Grouped-ID + /// - Grouped-ID + /// ], + /// Grouping-ID : [ + /// - Grouped-ID + /// - Grouped-ID + /// - Grouped-ID + /// ] + /// } + /// +// public typealias GroupByEntity = Never + +// public typealias GroupByKey = Never + + /** + Mapping another key to the entity id. + ``` + [ + Key: EntityID + Key: EntityID + Key: EntityID + ] + ``` + */ + public typealias Hash = HashTreeCollections.TreeDictionary + + public typealias Ordered = Array + + public typealias Set = Never + +} diff --git a/Sources/VergeNormalization/NonAtomicCounter.swift b/Sources/VergeNormalization/NonAtomicCounter.swift index c4bc392290..6191ebbb2c 100644 --- a/Sources/VergeNormalization/NonAtomicCounter.swift +++ b/Sources/VergeNormalization/NonAtomicCounter.swift @@ -5,7 +5,7 @@ public struct NonAtomicCounter: Hashable, Sendable { public init() {} - public consuming func increment() { + public mutating func increment() { value &+= 1 } diff --git a/Sources/VergeNormalization/NormalizedStorageType.swift b/Sources/VergeNormalization/NormalizedStorageType.swift index 86b591bd59..a0cc080887 100644 --- a/Sources/VergeNormalization/NormalizedStorageType.swift +++ b/Sources/VergeNormalization/NormalizedStorageType.swift @@ -3,6 +3,7 @@ public protocol NormalizedStorageType: Equatable { func finalizeTransaction(transaction: inout ModifyingTransaction) + static func compare(lhs: Self, rhs: Self) -> Bool } extension NormalizedStorageType { diff --git a/Sources/VergeNormalization/Selector.swift b/Sources/VergeNormalization/Selector.swift index 98377d8120..eee2668c73 100644 --- a/Sources/VergeNormalization/Selector.swift +++ b/Sources/VergeNormalization/Selector.swift @@ -1,14 +1,14 @@ public protocol TableSelector { associatedtype Entity: EntityType associatedtype Storage: NormalizedStorageType - func select(storage: Storage) -> Table + func select(storage: consuming Storage) -> Tables.Hash } public protocol StorageSelector { - associatedtype Source + associatedtype Source: Equatable associatedtype Storage: NormalizedStorageType - func select(source: Source) -> Storage + func select(source: consuming Source) -> Storage } extension StorageSelector { @@ -28,19 +28,23 @@ public struct AbsoluteTableSelector< _TableSelector: TableSelector > where _StorageSelector.Storage == _TableSelector.Storage { - public let storage: _StorageSelector - public let table: _TableSelector + public let storageSelector: _StorageSelector + public let tableSelector: _TableSelector public init( storage: consuming _StorageSelector, table: consuming _TableSelector ) { - self.storage = storage - self.table = table + self.storageSelector = storage + self.tableSelector = table } - public func select(source: _StorageSelector.Source) -> Table<_TableSelector.Entity> { - table.select(storage: storage.select(source: source)) + public func storage(source: consuming _StorageSelector.Source) -> _StorageSelector.Storage { + storageSelector.select(source: source) + } + + public func table(source: consuming _StorageSelector.Source) -> Tables.Hash<_TableSelector.Entity> { + tableSelector.select(storage: storageSelector.select(source: source)) } } diff --git a/Sources/VergeNormalization/Table.swift b/Sources/VergeNormalization/Table.swift deleted file mode 100644 index e6befa7e98..0000000000 --- a/Sources/VergeNormalization/Table.swift +++ /dev/null @@ -1,154 +0,0 @@ -import struct HashTreeCollections.TreeDictionary - -public struct Table: Equatable { - - public typealias Entity = Entity - - private var storage: TreeDictionary - public private(set) var updatedMarker = NonAtomicCounter() - - /// The number of entities in table - public var count: Int { - _read { yield storage.count } - } - - /// A Boolean value that indicates whether the dictionary is empty. - public var isEmpty: Bool { - _read { yield storage.isEmpty } - } - - public init(entities: TreeDictionary = .init()) { - self.storage = entities - } - - /// Returns all entity ids that stored. - public borrowing func allIDs() -> TreeDictionary.Keys { - return storage.keys - } - - /// Returns all entity that stored. - public borrowing func allEntities() -> some Collection { - return storage - } - - /** - Finds an entity by the identifier of the entity. - - Returns: An entity that found by identifier. Nil if the table does not have that entity. - */ - public borrowing func find(by id: consuming Entity.EntityID) -> Entity? { - return storage[id] - } - - /// Finds entities by set of ids. - /// The order of array would not be sorted, it depends on dictionary's buffer. - /// - /// if ids contains same id, result also contains same element. - /// - Parameter ids: sequence of Entity.ID - public borrowing func find(in ids: consuming S) -> [Entity] where S.Element == Entity.EntityID { - - return ids.reduce(into: [Entity]()) { (buf, id) in - guard let entity = storage[id] else { return } - buf.append(entity) - } - } - - /** - Updates the entity that already exsisting in the table. - - - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) - */ - @discardableResult - @inline(__always) - public mutating func updateExists( - id: consuming Entity.EntityID, - update: (inout Entity) throws -> Void - ) throws -> Entity { - - guard var current = storage[id] else { - throw ModifyingTransactionError.storedEntityNotFound - } - - try update(¤t) - precondition(current.entityID == id) - storage[id] = current - - updatedMarker.increment() - - return current - } - - /** - Updates the entity that already exsisting in the table. - - - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) - */ - @discardableResult - public mutating func updateIfExists( - id: consuming Entity.EntityID, - update: (inout Entity) throws -> Void - ) rethrows -> Entity? { - try? updateExists(id: id, update: update) - } - - /** - Inserts an entity - */ - @discardableResult - public mutating func insert(_ entity: consuming Entity) -> InsertionResult { - - let copied = copy entity - - storage[entity.entityID] = copied - - updatedMarker.increment() - - return .init(entity: copied) - } - - /** - Inserts a collection of the entity. - */ - @discardableResult - public mutating func insert(_ addingEntities: consuming S) -> [InsertionResult] where S.Element == Entity { - - let results = addingEntities.map { entity -> InsertionResult in - storage[entity.entityID] = entity - return .init(entity: entity) - } - - updatedMarker.increment() - - return results - } - - /** - Removes the entity by the identifier. - */ - public consuming func remove(_ id: Entity.EntityID) { - storage.removeValue(forKey: id) - updatedMarker.increment() - } - - /** - Removes the all of the entities in the table. - */ - public consuming func removeAll() { - storage.removeAll(where: { _ in true }) - updatedMarker.increment() - } -} - -extension Table { - /// An object indicates result of insertion - /// It can be used to create a getter object. - public struct InsertionResult { - public var entityID: Entity.EntityID { - entity.entityID - } - public let entity: Entity - - init(entity: consuming Entity) { - self.entity = entity - } - } -} diff --git a/Sources/VergeNormalization/Tables/TableType.swift b/Sources/VergeNormalization/Tables/TableType.swift new file mode 100644 index 0000000000..d393956316 --- /dev/null +++ b/Sources/VergeNormalization/Tables/TableType.swift @@ -0,0 +1,50 @@ + +/// a storage of the entity +public protocol TableType: Equatable { + + associatedtype Entity: EntityType + + typealias InsertionResult = VergeNormalization.InsertionResult + + var updatedMarker: NonAtomicCounter { get } + + var count: Int { get } + var isEmpty: Bool { get } + + borrowing func find(by id: consuming Entity.EntityID) -> Entity? + + borrowing func find(in ids: consuming some Sequence) -> [Entity] + + mutating func updateExists( + id: consuming Entity.EntityID, + update: (inout Entity) throws -> Void + ) throws -> Entity + + mutating func updateIfExists( + id: consuming Entity.EntityID, + update: (inout Entity) throws -> Void + ) rethrows -> Entity? + + @discardableResult + mutating func insert(_ entity: consuming Entity) -> InsertionResult + + @discardableResult + mutating func insert(_ addingEntities: consuming some Sequence) -> [InsertionResult] + + consuming func remove(_ id: Entity.EntityID) + + consuming func removeAll() +} + +/// An object indicates result of insertion +/// It can be used to create a getter object. +public struct InsertionResult { + public var entityID: Entity.EntityID { + entity.entityID + } + public let entity: Entity + + init(entity: consuming Entity) { + self.entity = entity + } +} diff --git a/Sources/VergeNormalization/Tables/Tables.Hash.swift b/Sources/VergeNormalization/Tables/Tables.Hash.swift new file mode 100644 index 0000000000..6266928fa4 --- /dev/null +++ b/Sources/VergeNormalization/Tables/Tables.Hash.swift @@ -0,0 +1,145 @@ +import struct HashTreeCollections.TreeDictionary + +extension Tables { + + /** + A table that stores entities with hash table. + */ + public struct Hash: TableType { + + public typealias Entity = Entity + + private var storage: TreeDictionary + public private(set) var updatedMarker = NonAtomicCounter() + + /// The number of entities in table + public var count: Int { + _read { yield storage.count } + } + + /// A Boolean value that indicates whether the dictionary is empty. + public var isEmpty: Bool { + _read { yield storage.isEmpty } + } + + public init(entities: TreeDictionary = .init()) { + self.storage = entities + } + + /// Returns all entity ids that stored. + public borrowing func allIDs() -> TreeDictionary.Keys { + return storage.keys + } + + /// Returns all entity that stored. + public borrowing func allEntities() -> some Collection { + return storage.values + } + + /** + Finds an entity by the identifier of the entity. + - Returns: An entity that found by identifier. Nil if the table does not have that entity. + */ + public borrowing func find(by id: consuming Entity.EntityID) -> Entity? { + return storage[id] + } + + /// Finds entities by set of ids. + /// The order of array would not be sorted, it depends on dictionary's buffer. + /// + /// if ids contains same id, result also contains same element. + /// - Parameter ids: sequence of Entity.ID + public borrowing func find(in ids: consuming some Sequence) -> [Entity] { + + return ids.reduce(into: [Entity]()) { (buf, id) in + guard let entity = storage[id] else { return } + buf.append(entity) + } + } + + /** + Updates the entity that already exsisting in the table. + + - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) + */ + @discardableResult + @inline(__always) + public mutating func updateExists( + id: consuming Entity.EntityID, + update: (inout Entity) throws -> Void + ) throws -> Entity { + + guard var current = storage[id] else { + throw ModifyingTransactionError.storedEntityNotFound + } + + try update(¤t) + precondition(current.entityID == id) + storage[id] = current + + updatedMarker.increment() + + return current + } + + /** + Updates the entity that already exsisting in the table. + + - Attention: Please don't change `EntityType.entityID` value. if we changed, the crash happens (precondition) + */ + @discardableResult + public mutating func updateIfExists( + id: consuming Entity.EntityID, + update: (inout Entity) throws -> Void + ) rethrows -> Entity? { + try? updateExists(id: id, update: update) + } + + /** + Inserts an entity + */ + @discardableResult + public mutating func insert(_ entity: consuming Entity) -> Self.InsertionResult { + + let copied = copy entity + + storage[entity.entityID] = copied + + updatedMarker.increment() + + return .init(entity: copied) + } + + /** + Inserts a collection of the entity. + */ + @discardableResult + public mutating func insert(_ addingEntities: consuming some Sequence) -> [Self.InsertionResult] { + + let results = addingEntities.map { entity -> Self.InsertionResult in + storage[entity.entityID] = entity + return .init(entity: entity) + } + + updatedMarker.increment() + + return results + } + + /** + Removes the entity by the identifier. + */ + public consuming func remove(_ id: Entity.EntityID) { + storage.removeValue(forKey: id) + updatedMarker.increment() + } + + /** + Removes the all of the entities in the table. + */ + public consuming func removeAll() { + storage.removeAll(where: { _ in true }) + updatedMarker.increment() + } + } +} diff --git a/Sources/VergeNormalization/Tables/Tables.swift b/Sources/VergeNormalization/Tables/Tables.swift new file mode 100644 index 0000000000..eb0b843f2f --- /dev/null +++ b/Sources/VergeNormalization/Tables/Tables.swift @@ -0,0 +1,4 @@ + +public enum Tables { + +} diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index 34fb8402a6..02b51d23b5 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -2,11 +2,13 @@ //@attached(member, names: arbitrary) //@attached(memberAttribute) @attached(extension, conformances: NormalizedStorageType, Equatable, names: named(Context), named(BBB), arbitrary) -public macro NormalizedStorage() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseMacro") +public macro NormalizedStorage() = #externalMacro(module: "VergeMacrosPlugin", type: "NormalizedStorageMacro") @attached(peer) -public macro TableAccessor() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseTableMacro") +public macro Table() = #externalMacro(module: "VergeMacrosPlugin", type: "TableMacro") +@attached(peer) +public macro Index() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseIndexMacro") #if DEBUG @@ -19,14 +21,14 @@ struct A: EntityType { @NormalizedStorage struct MyDatabase { - @TableAccessor - var user: Table + @Table + var user: Tables.Hash - @TableAccessor - var user2: Table = .init() + @Table + var user2: Tables.Hash = .init() - @TableAccessor - var user3: Table = .init() + @Table + var user3: Tables.Hash = .init() } extension MyDatabase { diff --git a/Sources/VergeNormalization/VergeNormalization.swift b/Sources/VergeNormalization/VergeNormalization.swift index 2d46c51b65..8b13789179 100644 --- a/Sources/VergeNormalization/VergeNormalization.swift +++ b/Sources/VergeNormalization/VergeNormalization.swift @@ -1,2 +1 @@ -@_exported import HashTreeCollections diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift index 8481fe0112..4a73eb1d87 100644 --- a/Sources/VergeNormalizationDerived/DispatcherType+.swift +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -5,11 +5,11 @@ extension DispatcherType { _TableSelector: TableSelector >( selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, - entityID: _TableSelector.Entity.EntityID + entityID: consuming _TableSelector.Entity.EntityID ) -> Derived> where _StorageSelector.Storage == _TableSelector.Storage, - _StorageSelector.Source == Changes + _StorageSelector.Source == Self.State { // TODO: caching @@ -29,11 +29,11 @@ extension DispatcherType { _TableSelector: TableSelector >( selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, - entityID: _TableSelector.Entity.EntityID + entityID: consuming _TableSelector.Entity.EntityID ) -> Derived> where _StorageSelector.Storage == _TableSelector.Storage, - _StorageSelector.Source == Changes + _StorageSelector.Source == Self.State { // TODO: caching @@ -53,10 +53,11 @@ private struct SingleEntityPipeline< _StorageSelector: StorageSelector, _TableSelector: TableSelector >: PipelineType -where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Source: ChangesType { +where _StorageSelector.Storage == _TableSelector.Storage { typealias Entity = _TableSelector.Entity - typealias Input = _StorageSelector.Source + typealias Input = Changes<_StorageSelector.Source> + typealias Storage = _StorageSelector.Storage typealias Output = EntityWrapper private let selector: AbsoluteTableSelector<_StorageSelector, _TableSelector> @@ -72,7 +73,7 @@ where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Sourc func yield(_ input: consuming Input) -> EntityWrapper { - let result = selector.select(source: input) + let result = selector.table(source: input.primitive) .find(by: entityID) return .init(id: entityID, entity: consume result) @@ -85,7 +86,16 @@ where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Sourc return .new(yield(input)) } - fatalError() + if NormalizedStorageComparisons.StorageComparison()(selector.storage(source: input.primitive), selector.storage(source: previous.primitive)) { + return .noUpdates + } + + if NormalizedStorageComparisons.TableComparison()(selector.table(source: input.primitive), selector.table(source: previous.primitive)) { + return .noUpdates + } + + return .new(yield(input)) + } } diff --git a/Tests/VergeMacrosTests/DatabaseMacroTests.swift b/Tests/VergeMacrosTests/DatabaseMacroTests.swift index 9aa6c945f4..20a3a982bf 100644 --- a/Tests/VergeMacrosTests/DatabaseMacroTests.swift +++ b/Tests/VergeMacrosTests/DatabaseMacroTests.swift @@ -6,7 +6,7 @@ import XCTest import VergeMacrosPlugin fileprivate let macros: [String : Macro.Type] = [ - "NormalizedStorage": DatabaseMacro.self + "NormalizedStorage": NormalizedStorageMacro.self ] final class DatabaseMacroTests: XCTestCase { diff --git a/Tests/VergeNormalizationDerivedTests/DemoState.swift b/Tests/VergeNormalizationDerivedTests/DemoState.swift new file mode 100644 index 0000000000..bcd92ff612 --- /dev/null +++ b/Tests/VergeNormalizationDerivedTests/DemoState.swift @@ -0,0 +1,59 @@ +import VergeNormalizationDerived + +struct DemoState: Equatable { + + var count: Int = 0 + + var db: Database = .init() + + struct DatabaseSelector: StorageSelector { + typealias Source = DemoState + typealias Storage = Database + + func select(source: consuming DemoState) -> Database { + source.db + } + } +} + +@NormalizedStorage +struct Database { + + @TableAccessor + var book: Tables.Hash = .init() + + @TableAccessor + var author: Tables.Hash = .init() + +} + +extension Database { + +} + +struct Book: EntityType, Hashable { + + typealias EntityIDRawType = String + + var entityID: EntityID { + .init(rawID) + } + + let rawID: String + let authorID: Author.EntityID + var name: String = "initial" +} + +struct Author: EntityType { + + typealias EntityIDRawType = String + + var entityID: EntityID { + .init(rawID) + } + + let rawID: String + var name: String = "" + + static let anonymous: Author = .init(rawID: "anonymous") +} diff --git a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift new file mode 100644 index 0000000000..39eab4e2bf --- /dev/null +++ b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift @@ -0,0 +1,42 @@ +import VergeNormalizationDerived +import XCTest + +final class VergeNormalizationDerivedTests: XCTestCase { + + func test_subscribe() { + + let exp = expectation(description: "wait") + + let store = Store( + initialState: .init() + ) + + let derived = store.derivedEntity( + selector: DemoState.DatabaseSelector().append(Database.book()), + entityID: Book.EntityID.init("1") + ) + + var received: [Book?] = [] + + derived.sinkState { value in + received.append(value.primitive.wrapped) + if value.primitive.wrapped != nil { + exp.fulfill() + } + } + .storeWhileSourceActive() + + _ = store.commit { + $0.db.performBatchUpdates { t in + t.modifying.book.insert(.init(rawID: "1", authorID: .init("muukii"))) + } + } + + wait(for: [exp]) + + XCTAssertEqual(received, [nil, .init(rawID: "1", authorID: .init("muukii"))]) + + withExtendedLifetime(derived, {}) + } + +} diff --git a/Tests/VergeNormalizationTests/Entities.swift b/Tests/VergeNormalizationTests/Entities.swift index 074192939d..a9227b3be7 100644 --- a/Tests/VergeNormalizationTests/Entities.swift +++ b/Tests/VergeNormalizationTests/Entities.swift @@ -30,9 +30,9 @@ struct Author: EntityType { @NormalizedStorage struct MyStorage { - @TableAccessor - var book: Table = .init() - @TableAccessor - var author: Table = .init() + @Table + var book: Tables.Hash = .init() + @Table + var author: Tables.Hash = .init() } diff --git a/Tests/VergeNormalizationTests/Source.swift b/Tests/VergeNormalizationTests/Source.swift index 1872ca50d7..979d3ca4de 100644 --- a/Tests/VergeNormalizationTests/Source.swift +++ b/Tests/VergeNormalizationTests/Source.swift @@ -15,4 +15,261 @@ final class Tests: XCTestCase { } + func test_init_count() { + + let db = MyStorage() + XCTAssertEqual(db.book.allEntities().count, 0) + } + + func testCommit() { + + var state = MyStorage() + + var transaction = state.beginBatchUpdates() + + let book = Book(rawID: "some", authorID: Author.anonymous.entityID) + transaction.modifying.book.insert(book) + + state.commitBatchUpdates(transaction: transaction) + + let a = state.book + let b = state.book + + XCTAssertEqual(a, b) + + } + + func testEqualityEntityTable() { + + var state = MyStorage() + + state.performBatchUpdates { (context) in + + let book = Book(rawID: "some", authorID: Author.anonymous.entityID) + context.modifying.book.insert(book) + } + + let a = state.book + let b = state.book + + XCTAssertEqual(a, b) + + } + + func testSimpleInsert() { + + var state = MyStorage() + + state.performBatchUpdates { (context) in + + let book = Book(rawID: "some", authorID: Author.anonymous.entityID) + context.modifying.book.insert(book) + } + + XCTAssertEqual(state.book.count, 1) + + } + + func testManagingOrderTable() { + + var state = MyStorage() + + state.performBatchUpdates { (context) in + + let book = Book(rawID: "some", authorID: Author.anonymous.entityID) + context.modifying.book.insert(book) + context.indexes.allBooks.append(book.entityID) + } + + XCTAssertEqual(state.entities.book.count, 1) + XCTAssertEqual(state.indexes.allBooks.count, 1) + + print(state.indexes.allBooks) + + state.performBatchUpdates { (context) -> Void in + context.modifying.book.delete(Book.EntityID.init("some")) + } + + XCTAssertEqual(state.entities.book.count, 0) + XCTAssertEqual(state.indexes.allBooks.count, 0) + + } + + func testUpdate() { + + var state = MyStorage() + + let id = Book.EntityID.init("some") + + state.performBatchUpdates { (context) in + + let book = Book(rawID: id.raw, authorID: Author.anonymous.entityID) + context.modifying.book.insert(book) + } + + XCTAssertNotNil(state.book.find(by: id)) + + state.performBatchUpdates { (context) in + + guard var book = context.modifying.book.find(by: id) else { + XCTFail() + return + } + book.name = "hello" + + context.modifying.book.insert(book) + } + + XCTAssertNotNil(state.book.find(by: id)) + XCTAssertNotNil(state.book.find(by: id)!.name == "hello") + + } + + func testUpdateIfExists() { + + var state = MyStorage() + + state.performBatchUpdates { (context) -> Void in + + context.modifying.author.insert(Author(rawID: "muukii", name: "muukii")) + + } + + state.performBatchUpdates { context in + + context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in + XCTAssertEqual(author.name, "muukii") + author.name = "Hiroshi" + } + + context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in + XCTAssertEqual(author.name, "Hiroshi") + author.name = "Kimura" + } + + context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in + XCTAssertEqual(author.name, "Kimura") + } + + } + + } + + func testGetAll() { + + var state = MyStorage() + + state.performBatchUpdates { (context) -> Void in + + context.modifying.author.insert(Author(rawID: "muukii", name: "muukii")) + + } + + state.performBatchUpdates { context in + + XCTAssertEqual(context.modifying.author.allEntities().first?.name, "muukii") + + context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in + XCTAssertEqual(author.name, "muukii") + author.name = "Hiroshi" + } + + XCTAssertEqual(context.modifying.author.allEntities().first?.name, "Hiroshi") + + context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in + XCTAssertEqual(author.name, "Hiroshi") + author.name = "Kimura" + } + + XCTAssertEqual(context.author.all().first?.name, "Kimura") + + context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in + XCTAssertEqual(author.name, "Kimura") + } + + } + + } + + func testDescription() { + + let authorID = Author.EntityID("author.id") + XCTAssertEqual(authorID.description, "(author.id)") + } + + func testFind() { + + var state = MyStorage() + + state.performBatchUpdates { (context) -> Void in + + for i in 0..<100 { + + let a = Author(rawID: "\(i)", name: "\(i)") + + context.modifying.author.insert(a) + context.modifying.book.insert(Book(rawID: "\(i)", authorID: a.entityID)) + + } + + } + + XCTAssertNotNil( + state.book.find(by: .init("\(1)")) + ) + + XCTAssertEqual( + state.book.find(in: [.init("\(1)"), .init("\(2)")]).count, + 2 + ) + + XCTAssertNotNil( + state.author.find(by: .init("\(1)")) + ) + + XCTAssertEqual( + state.author.find(in: [.init("\(1)"), .init("\(2)")]).count, + 2 + ) + + } + + func testDeletionAndInsertionInTransaction() { + + var state = MyStorage() + + let record = Author(rawID: "1", name: "1") + + state.performBatchUpdates { (context) -> Void in + context.modifying.author.insert(record) + } + + XCTAssertEqual( + state.author.allEntities().count, + 1 + ) + + state.performBatchUpdates { (context) -> Void in + context.modifying.author.removeAll() + context.modifying.author.insert(record) + } + + XCTAssertEqual( + state.author.allEntities().count, + 1 + ) + + state.performBatchUpdates { (context) -> Void in + context.modifying.author.removeAll() + context.modifying.author.insert([record]) + } + + XCTAssertEqual( + state.author.allEntities().count, + 1 + ) + + } + + } From 6dcc771344d4e4983bdf3864a84498565938d974 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 3 Sep 2023 02:14:46 +0900 Subject: [PATCH 08/16] :evergreen_tree: Update --- Sources/VergeMacrosPlugin/IndexMacro.swift | 12 ++++++++++- Sources/VergeMacrosPlugin/Plugin.swift | 2 +- .../VergeNormalization/Tables/TableType.swift | 4 ++-- .../Tables/Tables.Hash.swift | 4 ++-- .../VergeNormalization/Tables/Tables.swift | 1 - .../VergeNormalization+Macros.swift | 2 +- .../DemoState.swift | 4 ++-- Tests/VergeNormalizationTests/Entities.swift | 3 +++ Tests/VergeNormalizationTests/Source.swift | 20 ++++++++++--------- 9 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Sources/VergeMacrosPlugin/IndexMacro.swift b/Sources/VergeMacrosPlugin/IndexMacro.swift index 41754f8d3e..91e213d218 100644 --- a/Sources/VergeMacrosPlugin/IndexMacro.swift +++ b/Sources/VergeMacrosPlugin/IndexMacro.swift @@ -2,6 +2,16 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct DatabaseIndexMacro: Macro { +public struct IndexMacro: Macro { } + +extension IndexMacro: PeerMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + [] + } +} diff --git a/Sources/VergeMacrosPlugin/Plugin.swift b/Sources/VergeMacrosPlugin/Plugin.swift index 99e8a6c3c1..012741e669 100644 --- a/Sources/VergeMacrosPlugin/Plugin.swift +++ b/Sources/VergeMacrosPlugin/Plugin.swift @@ -11,6 +11,6 @@ struct Plugin: CompilerPlugin { NormalizedStorageMacro.self, TableMacro.self, - DatabaseIndexMacro.self + IndexMacro.self ] } diff --git a/Sources/VergeNormalization/Tables/TableType.swift b/Sources/VergeNormalization/Tables/TableType.swift index d393956316..de0fc6bc42 100644 --- a/Sources/VergeNormalization/Tables/TableType.swift +++ b/Sources/VergeNormalization/Tables/TableType.swift @@ -31,9 +31,9 @@ public protocol TableType: Equatable { @discardableResult mutating func insert(_ addingEntities: consuming some Sequence) -> [InsertionResult] - consuming func remove(_ id: Entity.EntityID) + mutating func remove(_ id: Entity.EntityID) - consuming func removeAll() + mutating func removeAll() } /// An object indicates result of insertion diff --git a/Sources/VergeNormalization/Tables/Tables.Hash.swift b/Sources/VergeNormalization/Tables/Tables.Hash.swift index 6266928fa4..60132fae42 100644 --- a/Sources/VergeNormalization/Tables/Tables.Hash.swift +++ b/Sources/VergeNormalization/Tables/Tables.Hash.swift @@ -129,7 +129,7 @@ extension Tables { /** Removes the entity by the identifier. */ - public consuming func remove(_ id: Entity.EntityID) { + public mutating func remove(_ id: Entity.EntityID) { storage.removeValue(forKey: id) updatedMarker.increment() } @@ -137,7 +137,7 @@ extension Tables { /** Removes the all of the entities in the table. */ - public consuming func removeAll() { + public mutating func removeAll() { storage.removeAll(where: { _ in true }) updatedMarker.increment() } diff --git a/Sources/VergeNormalization/Tables/Tables.swift b/Sources/VergeNormalization/Tables/Tables.swift index eb0b843f2f..661f82bef5 100644 --- a/Sources/VergeNormalization/Tables/Tables.swift +++ b/Sources/VergeNormalization/Tables/Tables.swift @@ -1,4 +1,3 @@ public enum Tables { - } diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index 02b51d23b5..2f92075205 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -8,7 +8,7 @@ public macro NormalizedStorage() = #externalMacro(module: "VergeMacrosPlugin", t public macro Table() = #externalMacro(module: "VergeMacrosPlugin", type: "TableMacro") @attached(peer) -public macro Index() = #externalMacro(module: "VergeMacrosPlugin", type: "DatabaseIndexMacro") +public macro Index() = #externalMacro(module: "VergeMacrosPlugin", type: "IndexMacro") #if DEBUG diff --git a/Tests/VergeNormalizationDerivedTests/DemoState.swift b/Tests/VergeNormalizationDerivedTests/DemoState.swift index bcd92ff612..4db7c284ce 100644 --- a/Tests/VergeNormalizationDerivedTests/DemoState.swift +++ b/Tests/VergeNormalizationDerivedTests/DemoState.swift @@ -19,10 +19,10 @@ struct DemoState: Equatable { @NormalizedStorage struct Database { - @TableAccessor + @Table var book: Tables.Hash = .init() - @TableAccessor + @Table var author: Tables.Hash = .init() } diff --git a/Tests/VergeNormalizationTests/Entities.swift b/Tests/VergeNormalizationTests/Entities.swift index a9227b3be7..b67110bf35 100644 --- a/Tests/VergeNormalizationTests/Entities.swift +++ b/Tests/VergeNormalizationTests/Entities.swift @@ -35,4 +35,7 @@ struct MyStorage { @Table var author: Tables.Hash = .init() + @Index + var bookIndex: Indexes.Ordered = .init() + } diff --git a/Tests/VergeNormalizationTests/Source.swift b/Tests/VergeNormalizationTests/Source.swift index 979d3ca4de..ae56bb5826 100644 --- a/Tests/VergeNormalizationTests/Source.swift +++ b/Tests/VergeNormalizationTests/Source.swift @@ -78,20 +78,22 @@ final class Tests: XCTestCase { let book = Book(rawID: "some", authorID: Author.anonymous.entityID) context.modifying.book.insert(book) - context.indexes.allBooks.append(book.entityID) + context.modifying.bookIndex.append(book.entityID) } - XCTAssertEqual(state.entities.book.count, 1) - XCTAssertEqual(state.indexes.allBooks.count, 1) + XCTAssertEqual(state.book.count, 1) + XCTAssertEqual(state.bookIndex.count, 1) - print(state.indexes.allBooks) + print(state.bookIndex) state.performBatchUpdates { (context) -> Void in - context.modifying.book.delete(Book.EntityID.init("some")) + context.modifying.book.remove(Book.EntityID.init("some")) } - XCTAssertEqual(state.entities.book.count, 0) - XCTAssertEqual(state.indexes.allBooks.count, 0) + XCTAssertEqual(state.book.count, 0) + + /// should not be deleted automatically + XCTAssertEqual(state.bookIndex.count, 1) } @@ -181,7 +183,7 @@ final class Tests: XCTestCase { author.name = "Kimura" } - XCTAssertEqual(context.author.all().first?.name, "Kimura") + XCTAssertEqual(context.modifying.author.allEntities().first?.name, "Kimura") context.modifying.author.updateIfExists(id: .init("muukii")) { (author) in XCTAssertEqual(author.name, "Kimura") @@ -194,7 +196,7 @@ final class Tests: XCTestCase { func testDescription() { let authorID = Author.EntityID("author.id") - XCTAssertEqual(authorID.description, "(author.id)") + XCTAssertEqual(authorID.description, "(author.id)") } func testFind() { From 1872a06d98476dea3bf91dad9f4b5b71e18832e3 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 3 Sep 2023 02:23:31 +0900 Subject: [PATCH 09/16] :evergreen_tree: Update --- .../DispatcherType+.swift | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift index 4a73eb1d87..47c17442a0 100644 --- a/Sources/VergeNormalizationDerived/DispatcherType+.swift +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -1,3 +1,5 @@ +import Foundation + extension DispatcherType { public func derivedEntity< @@ -24,13 +26,13 @@ extension DispatcherType { } - public func derivedEntity2< + public func derivedEntityNonNull< _StorageSelector: StorageSelector, _TableSelector: TableSelector >( selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, - entityID: consuming _TableSelector.Entity.EntityID - ) -> Derived> + entity: consuming _TableSelector.Entity + ) -> Derived> where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Source == Self.State @@ -39,8 +41,8 @@ extension DispatcherType { // TODO: caching return derived( - SingleEntityPipeline( - targetIdentifier: entityID, + NonNullSingleEntityPipeline( + initialEntity: entity, selector: selector ), queue: .passthrough @@ -71,7 +73,7 @@ where _StorageSelector.Storage == _TableSelector.Storage { self.selector = selector } - func yield(_ input: consuming Input) -> EntityWrapper { + func yield(_ input: consuming Input) -> Output { let result = selector.table(source: input.primitive) .find(by: entityID) @@ -80,7 +82,68 @@ where _StorageSelector.Storage == _TableSelector.Storage { } - func yieldContinuously(_ input: Input) -> Verge.ContinuousResult> { + func yieldContinuously(_ input: Input) -> Verge.ContinuousResult { + + guard let previous = input.previous else { + return .new(yield(input)) + } + + if NormalizedStorageComparisons.StorageComparison()(selector.storage(source: input.primitive), selector.storage(source: previous.primitive)) { + return .noUpdates + } + + if NormalizedStorageComparisons.TableComparison()(selector.table(source: input.primitive), selector.table(source: previous.primitive)) { + return .noUpdates + } + + return .new(yield(input)) + + } + +} + +private struct NonNullSingleEntityPipeline< + _StorageSelector: StorageSelector, + _TableSelector: TableSelector +>: PipelineType +where _StorageSelector.Storage == _TableSelector.Storage { + + typealias Entity = _TableSelector.Entity + typealias Input = Changes<_StorageSelector.Source> + typealias Storage = _StorageSelector.Storage + typealias Output = NonNullEntityWrapper + + private let selector: AbsoluteTableSelector<_StorageSelector, _TableSelector> + private let entityID: _TableSelector.Entity.EntityID + + private let latestValue: Entity + private let lock: NSLock = .init() + + init( + initialEntity: Entity, + selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector> + ) { + + self.entityID = initialEntity.entityID + self.latestValue = initialEntity + self.selector = selector + + } + + func yield(_ input: consuming Input) -> Output { + + let result = selector.table(source: input.primitive) + .find(by: entityID) + + if let result { + return .init(entity: result, isFallBack: false) + } else { + return .init(entity: latestValue, isFallBack: true) + } + + } + + func yieldContinuously(_ input: Input) -> Verge.ContinuousResult { guard let previous = input.previous else { return .new(yield(input)) From 37b6b49f5b5cd8a86c37f5976245e9f63b5efbab Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 3 Sep 2023 03:17:44 +0900 Subject: [PATCH 10/16] :evergreen_tree: Update --- .../NormalizedStorageMacro.swift | 65 ++++++++++++------- .../VergeNormalization+Macros.swift | 11 +--- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift b/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift index e16eaf9e0a..5b1653e95a 100644 --- a/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift +++ b/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift @@ -7,6 +7,12 @@ public struct NormalizedStorageMacro: Macro { } extension NormalizedStorageMacro: ExtensionMacro { + + struct Table { + let node: VariableDeclSyntax + let typeAnnotation: TypeAnnotationSyntax + } + public static func expansion( of node: SwiftSyntax.AttributeSyntax, attachedTo declaration: some SwiftSyntax.DeclGroupSyntax, @@ -19,19 +25,10 @@ extension NormalizedStorageMacro: ExtensionMacro { fatalError() } - let tableMembers = structDecl.memberBlock.members + let tables: [Table] = structDecl.memberBlock.members .compactMap { $0.decl.as(VariableDeclSyntax.self) } - .filter { - guard $0.bindings.count == 1 else { - return false - } - guard $0.bindings.first!.typeAnnotation != nil else { - return false - } - return true - } .filter { $0.attributes.contains { switch $0 { @@ -42,11 +39,31 @@ extension NormalizedStorageMacro: ExtensionMacro { } } } + .filter { + guard $0.bindings.count == 1 else { + context.addDiagnostics( + from: MacroError(message: "@Table macro does not support multiple binding, such as `let a, b = 0`"), + node: $0 + ) + return false + } + guard $0.bindings.first!.typeAnnotation != nil else { + context.addDiagnostics( + from: MacroError(message: "@Table macro requires a type annotation, such as `identifier: Type`"), + node: $0 + ) + return false + } + return true + } + .map { + return Table.init(node: $0, typeAnnotation: $0.bindings.first!.typeAnnotation!) + } - let comparator = { + let comparatorExtension = { - let markerComparators = tableMembers.map { member in - member.bindings.first!.pattern.trimmed + let markerComparators = tables.map { member in + member.node.bindings.first!.pattern.trimmed } .map { name in "guard lhs.\(name).updatedMarker == rhs.\(name).updatedMarker else { return lhs == rhs }" @@ -63,17 +80,19 @@ extension NormalizedStorageMacro: ExtensionMacro { }() - let selectors = { + let selectorsExtension = { - let decls = tableMembers.map { member in + let decls = tables.map { member in """ - struct \(member.bindings.first!.pattern.trimmed): TableSelector { - typealias _Table = \(member.bindings.first!.typeAnnotation!.type.description) + public struct \(member.node.bindings.first!.pattern.trimmed): TableSelector { + typealias _Table = \(member.node.bindings.first!.typeAnnotation!.type.description) typealias Entity = _Table.Entity typealias Storage = \(structDecl.name.trimmed) - func select(storage: Storage) -> _Table { - storage.\(member.bindings.first!.pattern.trimmed) + public let identifier: String = "\(member.node.bindings.first!.pattern.trimmed)" + + public func select(storage: Storage) -> _Table { + storage.\(member.node.bindings.first!.pattern.trimmed) } } """ @@ -85,10 +104,10 @@ extension NormalizedStorageMacro: ExtensionMacro { } """ as DeclSyntax).cast(ExtensionDeclSyntax.self) }() - + return [ - comparator, - selectors, + comparatorExtension, + selectorsExtension, (""" extension \(structDecl.name.trimmed): NormalizedStorageType {} """ as DeclSyntax).cast(ExtensionDeclSyntax.self), @@ -97,8 +116,6 @@ extension NormalizedStorageMacro: ExtensionMacro { """ as DeclSyntax).cast(ExtensionDeclSyntax.self), (""" extension \(structDecl.name.trimmed) { - typealias BBB = String - struct Context {} } """ as DeclSyntax).cast(ExtensionDeclSyntax.self) ] diff --git a/Sources/VergeNormalization/VergeNormalization+Macros.swift b/Sources/VergeNormalization/VergeNormalization+Macros.swift index 2f92075205..3ac0524b74 100644 --- a/Sources/VergeNormalization/VergeNormalization+Macros.swift +++ b/Sources/VergeNormalization/VergeNormalization+Macros.swift @@ -29,16 +29,9 @@ struct MyDatabase { @Table var user3: Tables.Hash = .init() -} - -extension MyDatabase { - - static func c(lhs: Self, rhs: Self) -> Bool { - - lhs.user.updatedMarker == rhs.user.updatedMarker - - } + @Table + var user4: Tables.Hash = .init() } private func play() { From 3ad89ca6e204f0244679e933b3f87e7c61de1e10 Mon Sep 17 00:00:00 2001 From: Muukii Date: Sun, 3 Sep 2023 03:35:10 +0900 Subject: [PATCH 11/16] :evergreen_tree: Update --- Sources/VergeNormalization/Selector.swift | 7 +++++-- Tests/VergeNormalizationDerivedTests/DemoState.swift | 6 ++++++ .../VergeNormalizationDerivedTests.swift | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/VergeNormalization/Selector.swift b/Sources/VergeNormalization/Selector.swift index eee2668c73..66ef87d7d3 100644 --- a/Sources/VergeNormalization/Selector.swift +++ b/Sources/VergeNormalization/Selector.swift @@ -1,4 +1,4 @@ -public protocol TableSelector { +public protocol TableSelector { associatedtype Entity: EntityType associatedtype Storage: NormalizedStorageType func select(storage: consuming Storage) -> Tables.Hash @@ -13,7 +13,7 @@ public protocol StorageSelector { extension StorageSelector { - public func append<_TableSelector: TableSelector>( + public func appending<_TableSelector: TableSelector>( _ tableSelector: consuming _TableSelector ) -> AbsoluteTableSelector @@ -28,6 +28,9 @@ public struct AbsoluteTableSelector< _TableSelector: TableSelector > where _StorageSelector.Storage == _TableSelector.Storage { + public typealias Storage = _StorageSelector.Storage + public typealias Entity = _TableSelector.Entity + public let storageSelector: _StorageSelector public let tableSelector: _TableSelector diff --git a/Tests/VergeNormalizationDerivedTests/DemoState.swift b/Tests/VergeNormalizationDerivedTests/DemoState.swift index 4db7c284ce..0c7f48951d 100644 --- a/Tests/VergeNormalizationDerivedTests/DemoState.swift +++ b/Tests/VergeNormalizationDerivedTests/DemoState.swift @@ -16,6 +16,12 @@ struct DemoState: Equatable { } } +extension StorageSelector where Self == DemoState.DatabaseSelector { + static var db: Self { + DemoState.DatabaseSelector() + } +} + @NormalizedStorage struct Database { diff --git a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift index 39eab4e2bf..7ce769e9db 100644 --- a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift +++ b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift @@ -12,7 +12,7 @@ final class VergeNormalizationDerivedTests: XCTestCase { ) let derived = store.derivedEntity( - selector: DemoState.DatabaseSelector().append(Database.book()), + selector: DemoState.DatabaseSelector().appending(Database.book()), entityID: Book.EntityID.init("1") ) From d358f1107d288cfdd81c59dd7422b8326d658de1 Mon Sep 17 00:00:00 2001 From: Muukii Date: Mon, 11 Sep 2023 17:26:18 +0900 Subject: [PATCH 12/16] :evergreen_tree: Update --- Sources/Verge/Library/VergeConcurrency.swift | 105 +++++++++++- Sources/Verge/Store/Store.swift | 35 ++++ .../NormalizedStorageMacro.swift | 2 +- Sources/VergeNormalization/Comparison.swift | 4 +- Sources/VergeNormalization/Selector.swift | 71 +++++++- .../DispatcherType+.swift | 161 ++++++++++++++---- .../DemoState.swift | 3 + .../VergeNormalizationDerivedTests.swift | 42 ++++- 8 files changed, 375 insertions(+), 48 deletions(-) diff --git a/Sources/Verge/Library/VergeConcurrency.swift b/Sources/Verge/Library/VergeConcurrency.swift index 972fd1f2ef..34f0f6ae1c 100644 --- a/Sources/Verge/Library/VergeConcurrency.swift +++ b/Sources/Verge/Library/VergeConcurrency.swift @@ -129,6 +129,7 @@ public enum VergeConcurrency { } /// An atomic variable. + @propertyWrapper public final class UnfairLockAtomic: @unchecked Sendable { public var unsafelyWrappedValue: Value { @@ -143,21 +144,36 @@ public enum VergeConcurrency { get { return withValue { $0 } } - set(newValue) { swap(newValue) } } - + + public var wrappedValue: Value { + get { + return withValue { $0 } + } + set(newValue) { + swap(newValue) + } + } + /// Initialize the variable with the given initial value. /// /// - parameters: /// - value: Initial value for `self`. - public init(_ value: Value) { - _value = value + public init(_ wrappedValue: Value) { + _value = wrappedValue lock = .init() } - + + public init(wrappedValue: Value) { + _value = wrappedValue + lock = .init() + } + + public var projectedValue: UnfairLockAtomic { self } + /// Atomically modifies the variable. /// /// - parameters: @@ -203,4 +219,83 @@ public enum VergeConcurrency { } } + /// A container that initializes value when it needs. + /// + /// Supports multi-threading. + @propertyWrapper + public final class AtomicLazy: @unchecked Sendable { + + private enum State { + case initialized(T) + case notInitialized + } + + public typealias Initializer = () -> T + + private var _onInitialized: (T) -> Void = { _ in } + + private let lock: UnfairLock = .init() + + public var wrappedValue: T { + + lock.lock() + defer { + lock.unlock() + } + + return unsafeValue + } + + public var projectedValue: AtomicLazy { + self + } + + @discardableResult + public func modify(_ action: (inout T) throws -> Result) rethrows -> Result { + lock.lock() + defer { lock.unlock() } + + var new = unsafeValue + let result = try action(&new) + self._synchronized_state = .initialized(new) + return consume result + } + + private var _synchronized_state: State = .notInitialized + + private var unsafeValue: T { + get { + switch _synchronized_state { + case .notInitialized: + let value = initializer() + _onInitialized(value) + self._synchronized_state = .initialized(value) + self.initializer = nil + return value + case .initialized(let value): + return value + } + } + } + + private var initializer: Initializer! + + public init(_ initializer: @escaping Initializer) { + self.initializer = initializer + } + + public init(wrappedValue initializer: @autoclosure @escaping Initializer) { + self.initializer = initializer + } + + /// Set closure on value initialized. + /// the closure would be called on thread which value initialized. + @discardableResult + public func onInitialized(_ perform: @escaping (T) -> Void) -> Self { + _onInitialized = perform + return self + } + } + + } diff --git a/Sources/Verge/Store/Store.swift b/Sources/Verge/Store/Store.swift index b858abff45..c0751eeede 100644 --- a/Sources/Verge/Store/Store.swift +++ b/Sources/Verge/Store/Store.swift @@ -107,6 +107,12 @@ open class Store: EventEmitter<_StoreEvent, AnyObject> = .init(keyOptions: [.copyIn, .objectPersonality], valueOptions: [.weakMemory]) + + @_spi(NormalizedStorage) + @VergeConcurrency.AtomicLazy public var _nonnull_derivedCache: NSMapTable, AnyObject> = .init(keyOptions: [.copyIn, .objectPersonality], valueOptions: [.weakMemory]) + // MARK: - Deinit deinit { @@ -815,3 +821,32 @@ extension Store { } } + +public final class KeyObject: NSObject, NSCopying { + + public func copy(with zone: NSZone? = nil) -> Any { + return KeyObject(content: content) + } + + public let content: Content + + public init(content: consuming Content) { + self.content = content + } + + public override var hash: Int { + content.hashValue + } + + public override func isEqual(_ object: Any?) -> Bool { + + guard let other = object as? KeyObject else { + return false + } + + guard content == other.content else { return false } + + return true + } + +} diff --git a/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift b/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift index 5b1653e95a..87323e2973 100644 --- a/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift +++ b/Sources/VergeMacrosPlugin/NormalizedStorageMacro.swift @@ -84,7 +84,7 @@ extension NormalizedStorageMacro: ExtensionMacro { let decls = tables.map { member in """ - public struct \(member.node.bindings.first!.pattern.trimmed): TableSelector { + public struct TableSelector_\(member.node.bindings.first!.pattern.trimmed): TableSelector { typealias _Table = \(member.node.bindings.first!.typeAnnotation!.type.description) typealias Entity = _Table.Entity typealias Storage = \(structDecl.name.trimmed) diff --git a/Sources/VergeNormalization/Comparison.swift b/Sources/VergeNormalization/Comparison.swift index ddfa613c4f..ca559895bc 100644 --- a/Sources/VergeNormalization/Comparison.swift +++ b/Sources/VergeNormalization/Comparison.swift @@ -14,9 +14,9 @@ public enum NormalizedStorageComparisons { } /// Returns true if the table of the entity in database has no changes. - public struct TableComparison: Comparison { + public struct TableComparison: Comparison { - public typealias Input = Tables.Hash + public typealias Input = Table public init() {} diff --git a/Sources/VergeNormalization/Selector.swift b/Sources/VergeNormalization/Selector.swift index 66ef87d7d3..9e0a764bf7 100644 --- a/Sources/VergeNormalization/Selector.swift +++ b/Sources/VergeNormalization/Selector.swift @@ -1,18 +1,75 @@ -public protocol TableSelector { - associatedtype Entity: EntityType + +public protocol TableSelector: Hashable { associatedtype Storage: NormalizedStorageType - func select(storage: consuming Storage) -> Tables.Hash + associatedtype Table: TableType + func select(storage: consuming Storage) -> Table +} + +public struct KeyPathTableSelector< + Storage: NormalizedStorageType, + Table: TableType +>: TableSelector, Equatable { + + public let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func select(storage: consuming Storage) -> Table { + storage[keyPath: keyPath] + } + } -public protocol StorageSelector { +extension TableSelector { + + public static func keyPath< + Storage: NormalizedStorageType, + Table: TableType + >( + _ keyPath: KeyPath + ) -> Self where Self == KeyPathTableSelector { + return .init(keyPath: keyPath) + } +} + +public protocol StorageSelector: Hashable { associatedtype Source: Equatable associatedtype Storage: NormalizedStorageType func select(source: consuming Source) -> Storage } +public struct KeyPathStorageSelector< + Source: Equatable, + Storage: NormalizedStorageType +>: StorageSelector, Equatable { + + public let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func select(source: consuming Source) -> Storage { + source[keyPath: keyPath] + } + +} + extension StorageSelector { + public static func keyPath< + Source: Equatable, + Storage: NormalizedStorageType + > + ( + _ keyPath: KeyPath + ) -> Self where Self == KeyPathStorageSelector { + return .init(keyPath: keyPath) + } + public func appending<_TableSelector: TableSelector>( _ tableSelector: consuming _TableSelector ) @@ -26,10 +83,10 @@ extension StorageSelector { public struct AbsoluteTableSelector< _StorageSelector: StorageSelector, _TableSelector: TableSelector -> where _StorageSelector.Storage == _TableSelector.Storage { +>: Hashable where _StorageSelector.Storage == _TableSelector.Storage { public typealias Storage = _StorageSelector.Storage - public typealias Entity = _TableSelector.Entity + public typealias Entity = _TableSelector.Table.Entity public let storageSelector: _StorageSelector public let tableSelector: _TableSelector @@ -46,7 +103,7 @@ public struct AbsoluteTableSelector< storageSelector.select(source: source) } - public func table(source: consuming _StorageSelector.Source) -> Tables.Hash<_TableSelector.Entity> { + public func table(source: consuming _StorageSelector.Source) -> _TableSelector.Table { tableSelector.select(storage: storageSelector.select(source: source)) } diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift index 47c17442a0..02de5a0d66 100644 --- a/Sources/VergeNormalizationDerived/DispatcherType+.swift +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -1,28 +1,116 @@ import Foundation +@_spi(NormalizedStorage) import Verge + +extension StoreType { + + public func normalizedStorage(_ selector: Selector) -> NormalizedStoragePath { + .init(store: self, storageSelector: selector) + } + +} + +public struct NormalizedStoragePath< + Store: DispatcherType, + _StorageSelector: StorageSelector +>: ~Copyable where Store.State == _StorageSelector.Source { + + public typealias Storage = _StorageSelector.Storage + unowned let store: Store + let storageSelector: _StorageSelector + + public init( + store: Store, + storageSelector: _StorageSelector + ) { + self.store = store + self.storageSelector = storageSelector + } + + public func table( + _ selector: Selector + ) -> NormalizedStorageTablePath where Selector.Storage == _StorageSelector.Storage { + return .init( + store: store, + storageSelector: storageSelector, + tableSelector: selector + ) + } +} + +public struct NormalizedStorageTablePath< + Store: DispatcherType, + _StorageSelector: StorageSelector, + _TableSelector: TableSelector +>: ~Copyable where _StorageSelector.Storage == _TableSelector.Storage, Store.State == _StorageSelector.Source { + + unowned let store: Store + let storageSelector: _StorageSelector + let tableSelector: _TableSelector + + public func derivedEntity( + entityID: consuming _TableSelector.Table.Entity.EntityID + ) -> Derived> { + + return store.derivedEntity( + selector: .init(storage: storageSelector, table: tableSelector), + entityID: entityID + ) + + } + + public func derivedEntityNonNull( + tableSelector: consuming _TableSelector, + entity: consuming _TableSelector.Table.Entity + ) -> Derived> { + + return store.derivedEntityNonNull( + selector: .init(storage: storageSelector, table: tableSelector), + entity: entity + ) + + } + +} + extension DispatcherType { public func derivedEntity< _StorageSelector: StorageSelector, _TableSelector: TableSelector >( - selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, - entityID: consuming _TableSelector.Entity.EntityID - ) -> Derived> + selector: AbsoluteTableSelector<_StorageSelector, _TableSelector>, + entityID: consuming _TableSelector.Table.Entity.EntityID + ) -> Derived> where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Source == Self.State { - // TODO: caching + return store.asStore().$_derivedCache.modify { cache in - return derived( - SingleEntityPipeline( - targetIdentifier: entityID, - selector: selector - ), - queue: .passthrough - ) + typealias _Derived = Derived.Output> + + let key = KeyObject(content: AnyHashable(copy selector)) + + if let cached = cache.object(forKey: key) { + return cached as! _Derived + } else { + + let new = derived( + SingleEntityPipeline( + targetIdentifier: entityID, + selector: selector + ), + queue: .passthrough + ) + + cache.setObject(new, forKey: key) + + return new as _Derived + } + + } } @@ -30,23 +118,38 @@ extension DispatcherType { _StorageSelector: StorageSelector, _TableSelector: TableSelector >( - selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector>, - entity: consuming _TableSelector.Entity - ) -> Derived> + selector: AbsoluteTableSelector<_StorageSelector, _TableSelector>, + entity: consuming _TableSelector.Table.Entity + ) -> Derived> where _StorageSelector.Storage == _TableSelector.Storage, _StorageSelector.Source == Self.State { - // TODO: caching + return store.asStore().$_nonnull_derivedCache.modify { cache in - return derived( - NonNullSingleEntityPipeline( - initialEntity: entity, - selector: selector - ), - queue: .passthrough - ) + typealias _Derived = Derived.Output> + + let key = KeyObject(content: AnyHashable(copy selector)) + + if let cached = cache.object(forKey: key) { + return cached as! _Derived + } else { + + let new = derived( + NonNullSingleEntityPipeline( + initialEntity: entity, + selector: selector + ), + queue: .passthrough + ) + + cache.setObject(new, forKey: key) + + return new as _Derived + } + + } } } @@ -57,16 +160,16 @@ private struct SingleEntityPipeline< >: PipelineType where _StorageSelector.Storage == _TableSelector.Storage { - typealias Entity = _TableSelector.Entity + typealias Entity = _TableSelector.Table.Entity typealias Input = Changes<_StorageSelector.Source> typealias Storage = _StorageSelector.Storage typealias Output = EntityWrapper private let selector: AbsoluteTableSelector<_StorageSelector, _TableSelector> - private let entityID: _TableSelector.Entity.EntityID + private let entityID: Entity.EntityID init( - targetIdentifier: _TableSelector.Entity.EntityID, + targetIdentifier: Entity.EntityID, selector: consuming AbsoluteTableSelector<_StorageSelector, _TableSelector> ) { self.entityID = targetIdentifier @@ -92,7 +195,7 @@ where _StorageSelector.Storage == _TableSelector.Storage { return .noUpdates } - if NormalizedStorageComparisons.TableComparison()(selector.table(source: input.primitive), selector.table(source: previous.primitive)) { + if NormalizedStorageComparisons.TableComparison<_TableSelector.Table>()(selector.table(source: input.primitive), selector.table(source: previous.primitive)) { return .noUpdates } @@ -108,13 +211,13 @@ private struct NonNullSingleEntityPipeline< >: PipelineType where _StorageSelector.Storage == _TableSelector.Storage { - typealias Entity = _TableSelector.Entity + typealias Entity = _TableSelector.Table.Entity typealias Input = Changes<_StorageSelector.Source> typealias Storage = _StorageSelector.Storage typealias Output = NonNullEntityWrapper private let selector: AbsoluteTableSelector<_StorageSelector, _TableSelector> - private let entityID: _TableSelector.Entity.EntityID + private let entityID: Entity.EntityID private let latestValue: Entity private let lock: NSLock = .init() @@ -153,7 +256,7 @@ where _StorageSelector.Storage == _TableSelector.Storage { return .noUpdates } - if NormalizedStorageComparisons.TableComparison()(selector.table(source: input.primitive), selector.table(source: previous.primitive)) { + if NormalizedStorageComparisons.TableComparison<_TableSelector.Table>()(selector.table(source: input.primitive), selector.table(source: previous.primitive)) { return .noUpdates } diff --git a/Tests/VergeNormalizationDerivedTests/DemoState.swift b/Tests/VergeNormalizationDerivedTests/DemoState.swift index 0c7f48951d..2395183397 100644 --- a/Tests/VergeNormalizationDerivedTests/DemoState.swift +++ b/Tests/VergeNormalizationDerivedTests/DemoState.swift @@ -28,6 +28,9 @@ struct Database { @Table var book: Tables.Hash = .init() + @Table + var book2: Tables.Hash = .init() + @Table var author: Tables.Hash = .init() diff --git a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift index 7ce769e9db..776f24b51a 100644 --- a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift +++ b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift @@ -1,6 +1,14 @@ import VergeNormalizationDerived import XCTest +extension Store where State == DemoState { + + var database: NormalizedStoragePath { + return .init(store: self, storageSelector: .init()) + } + +} + final class VergeNormalizationDerivedTests: XCTestCase { func test_subscribe() { @@ -11,10 +19,10 @@ final class VergeNormalizationDerivedTests: XCTestCase { initialState: .init() ) - let derived = store.derivedEntity( - selector: DemoState.DatabaseSelector().appending(Database.book()), - entityID: Book.EntityID.init("1") - ) + let derived = store + .normalizedStorage(.keyPath(\.db)) + .table(.keyPath(\.book)) + .derivedEntity(entityID: Book.EntityID.init("1")) var received: [Book?] = [] @@ -39,4 +47,30 @@ final class VergeNormalizationDerivedTests: XCTestCase { withExtendedLifetime(derived, {}) } + func test_cache() { + + let store = Store( + initialState: .init() + ) + + let derived1 = store + .normalizedStorage(.keyPath(\.db)) + .table(.keyPath(\.book)) + .derivedEntity(entityID: Book.EntityID.init("1")) + + let derived2 = store + .normalizedStorage(.keyPath(\.db)) + .table(.keyPath(\.book)) + .derivedEntity(entityID: Book.EntityID.init("1")) + + let derived3 = store + .normalizedStorage(.keyPath(\.db)) + .table(.keyPath(\.book2)) + .derivedEntity(entityID: Book.EntityID.init("1")) + + XCTAssert(derived1 === derived2) + XCTAssert(derived2 !== derived3) + + } + } From c69193cd35a747ddb1b183864f072264ce57e6c8 Mon Sep 17 00:00:00 2001 From: Muukii Date: Mon, 11 Sep 2023 22:53:52 +0900 Subject: [PATCH 13/16] :zap: WIP --- Sources/VergeNormalization/Selector.swift | 2 +- .../DerivedResult.swift | 35 ++++ .../DispatcherType+.swift | 170 ++++++++++++++++-- Sources/VergeORM/Derived+ORM.swift | 26 --- .../VergeNormalizationDerivedTests.swift | 8 +- 5 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 Sources/VergeNormalizationDerived/DerivedResult.swift diff --git a/Sources/VergeNormalization/Selector.swift b/Sources/VergeNormalization/Selector.swift index 9e0a764bf7..42c3c4ff04 100644 --- a/Sources/VergeNormalization/Selector.swift +++ b/Sources/VergeNormalization/Selector.swift @@ -70,7 +70,7 @@ extension StorageSelector { return .init(keyPath: keyPath) } - public func appending<_TableSelector: TableSelector>( + public consuming func appending<_TableSelector: TableSelector>( _ tableSelector: consuming _TableSelector ) -> AbsoluteTableSelector diff --git a/Sources/VergeNormalizationDerived/DerivedResult.swift b/Sources/VergeNormalizationDerived/DerivedResult.swift new file mode 100644 index 0000000000..6b7623812d --- /dev/null +++ b/Sources/VergeNormalizationDerived/DerivedResult.swift @@ -0,0 +1,35 @@ +// +// File.swift +// +// +// Created by Muukii on 2023/09/11. +// + +import Foundation +import Verge + +/// A result instance that contains created Derived object +/// While creating non-null derived from entity id, some entity may be not founded. +/// Created derived object are stored in hashed storage to the consumer can check if the entity was not found by the id. +public struct DerivedResult { + + /// A dictionary of Derived that stored by id + /// It's faster than filtering values array to use this dictionary to find missing id or created id. + public private(set) var storage: [Entity.EntityID : Derived] = [:] + + /// An array of Derived that orderd by specified the order of id. + public private(set) var values: [Derived] + + public init() { + self.storage = [:] + self.values = [] + } + + public mutating func append(derived: Derived, id: Entity.EntityID) { + storage[id] = derived + values.append(derived) + } + +} + +public typealias NonNullDerivedResult = DerivedResult diff --git a/Sources/VergeNormalizationDerived/DispatcherType+.swift b/Sources/VergeNormalizationDerived/DispatcherType+.swift index 02de5a0d66..02c9edbea9 100644 --- a/Sources/VergeNormalizationDerived/DispatcherType+.swift +++ b/Sources/VergeNormalizationDerived/DispatcherType+.swift @@ -2,6 +2,10 @@ import Foundation @_spi(NormalizedStorage) import Verge +public enum NormalizedStorageError: Swift.Error { + case notFoundEntityToMakeDerived +} + extension StoreType { public func normalizedStorage(_ selector: Selector) -> NormalizedStoragePath { @@ -39,18 +43,20 @@ public struct NormalizedStoragePath< } public struct NormalizedStorageTablePath< - Store: DispatcherType, + Store: StoreType, _StorageSelector: StorageSelector, _TableSelector: TableSelector >: ~Copyable where _StorageSelector.Storage == _TableSelector.Storage, Store.State == _StorageSelector.Source { + public typealias Entity = _TableSelector.Table.Entity + unowned let store: Store let storageSelector: _StorageSelector let tableSelector: _TableSelector - public func derivedEntity( - entityID: consuming _TableSelector.Table.Entity.EntityID - ) -> Derived> { + public func derived( + from entityID: consuming Entity.EntityID + ) -> Derived> { return store.derivedEntity( selector: .init(storage: storageSelector, table: tableSelector), @@ -59,10 +65,19 @@ public struct NormalizedStorageTablePath< } - public func derivedEntityNonNull( - tableSelector: consuming _TableSelector, - entity: consuming _TableSelector.Table.Entity - ) -> Derived> { + // MARK: - NonNull + + /// Returns a derived object that provides a concrete entity according to the updating source state + /// It uses the last value if the entity has been removed source. + /// You can get a flag that indicates whether the entity is live or removed which from `NonNullEntityWrapper` + /// + /// If you call this method in many time, it's not so big issue. + /// Because, the backing derived-object to construct itself would be cached. + /// A pointer of the result derived object will be different from each other, but the backing source will be shared. + /// + public func derivedNonNull( + entity: consuming Entity + ) -> Derived> { return store.derivedEntityNonNull( selector: .init(storage: storageSelector, table: tableSelector), @@ -71,9 +86,138 @@ public struct NormalizedStorageTablePath< } + /// Returns a derived object that provides a concrete entity according to the updating source state + /// It uses the last value if the entity has been removed source. + /// You can get a flag that indicates whether the entity is live or removed which from `NonNullEntityWrapper` + /// + /// If you call this method in many time, it's not so big issue. + /// Because, the backing derived-object to construct itself would be cached. + /// A pointer of the result derived object will be different from each other, but the backing source will be shared. + /// + public func derivedNonNull( + entityID: consuming Entity.EntityID + ) throws -> Derived> { + + let _initialValue = storageSelector + .appending(tableSelector) + .table(source: store.state.primitive) + .find(by: entityID) + + guard let initalValue = _initialValue else { + throw NormalizedStorageError.notFoundEntityToMakeDerived + } + + return store.derivedEntityNonNull( + selector: .init(storage: storageSelector, table: tableSelector), + entity: initalValue + ) + + } + + public func derivedNonNull( + entities: consuming some Sequence + ) -> NonNullDerivedResult { + + var result = NonNullDerivedResult() + + for entity in entities { + result.append( + derived: self.derivedNonNull(entity: entity), + id: entity.entityID + ) + } + + return result + + } + + /// Returns a derived object that provides a concrete entity according to the updating source state + /// It uses the last value if the entity has been removed source. + /// You can get a flag that indicates whether the entity is live or removed which from `NonNullEntityWrapper` + /// + /// If you call this method in many time, it's not so big issue. + /// Because, the backing derived-object to construct itself would be cached. + /// A pointer of the result derived object will be different from each other, but the backing source will be shared. + /// + public func derivedNonNull( + entityIDs: consuming some Sequence + ) -> NonNullDerivedResult { + + var result = NonNullDerivedResult() + + for id in entityIDs { + do { + result.append( + derived: try self.derivedNonNull(entityID: id), + id: id + ) + } catch { + // FIXME: + } + } + + return result + } + + /// Returns a derived object that provides a concrete entity according to the updating source state + /// It uses the last value if the entity has been removed source. + /// You can get a flag that indicates whether the entity is live or removed which from `NonNullEntityWrapper` + /// + /// If you call this method in many time, it's not so big issue. + /// Because, the backing derived-object to construct itself would be cached. + /// A pointer of the result derived object will be different from each other, but the backing source will be shared. + /// + public func derivedNonNull( + entityIDs: consuming Set + ) -> NonNullDerivedResult { + + var result = NonNullDerivedResult() + + for id in entityIDs { + do { + result.append( + derived: try self.derivedNonNull(entityID: id), + id: id + ) + } catch { + // FIXME: + } + } + + return result + } + + /// Returns a derived object that provides a concrete entity according to the updating source state + /// It uses the last value if the entity has been removed source. + /// You can get a flag that indicates whether the entity is live or removed which from `NonNullEntityWrapper` + /// + /// If you call this method in many time, it's not so big issue. + /// Because, the backing derived-object to construct itself would be cached. + /// A pointer of the result derived object will be different from each other, but the backing source will be shared. + /// + public func derivedNonNull( + insertionResult: InsertionResult + ) -> Entity.NonNullDerived { + derivedNonNull(entity: insertionResult.entity) + } + + /// Returns a derived object that provides a concrete entity according to the updating source state + /// It uses the last value if the entity has been removed source. + /// You can get a flag that indicates whether the entity is live or removed which from `NonNullEntityWrapper` + /// + /// If you call this method in many time, it's not so big issue. + /// Because, the backing derived-object to construct itself would be cached. + /// A pointer of the result derived object will be different from each other, but the backing source will be shared. + /// + @inline(__always) + public func derivedNonNull( + insertionResults: some Sequence> + ) -> NonNullDerivedResult { + derivedNonNull(entities: insertionResults.map { $0.entity }) + } } -extension DispatcherType { +extension StoreType { public func derivedEntity< _StorageSelector: StorageSelector, @@ -87,7 +231,7 @@ extension DispatcherType { _StorageSelector.Source == Self.State { - return store.asStore().$_derivedCache.modify { cache in + return asStore().$_derivedCache.modify { cache in typealias _Derived = Derived.Output> @@ -97,7 +241,7 @@ extension DispatcherType { return cached as! _Derived } else { - let new = derived( + let new = asStore().derived( SingleEntityPipeline( targetIdentifier: entityID, selector: selector @@ -126,7 +270,7 @@ extension DispatcherType { _StorageSelector.Source == Self.State { - return store.asStore().$_nonnull_derivedCache.modify { cache in + return asStore().$_nonnull_derivedCache.modify { cache in typealias _Derived = Derived.Output> @@ -136,7 +280,7 @@ extension DispatcherType { return cached as! _Derived } else { - let new = derived( + let new = asStore().derived( NonNullSingleEntityPipeline( initialEntity: entity, selector: selector diff --git a/Sources/VergeORM/Derived+ORM.swift b/Sources/VergeORM/Derived+ORM.swift index bc500f02a2..875fbe4de7 100644 --- a/Sources/VergeORM/Derived+ORM.swift +++ b/Sources/VergeORM/Derived+ORM.swift @@ -94,8 +94,6 @@ fileprivate final class _NonNullDerivedObjectCache { } -public typealias NonNullDerivedResult = DerivedResult - // MARK: - Primitive operators fileprivate var _derivedContainerAssociated: Void? @@ -133,30 +131,6 @@ extension DispatcherType { } -/// A result instance that contains created Derived object -/// While creating non-null derived from entity id, some entity may be not founded. -/// Created derived object are stored in hashed storage to the consumer can check if the entity was not found by the id. -public struct DerivedResult { - - /// A dictionary of Derived that stored by id - /// It's faster than filtering values array to use this dictionary to find missing id or created id. - public private(set) var storage: [Entity.EntityID : Derived] = [:] - - /// An array of Derived that orderd by specified the order of id. - public private(set) var values: [Derived] - - public init() { - self.storage = [:] - self.values = [] - } - - public mutating func append(derived: Derived, id: Entity.EntityID) { - storage[id] = derived - values.append(derived) - } - -} - /** Do not retain, use as just method-chain */ diff --git a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift index 776f24b51a..970e808c98 100644 --- a/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift +++ b/Tests/VergeNormalizationDerivedTests/VergeNormalizationDerivedTests.swift @@ -22,7 +22,7 @@ final class VergeNormalizationDerivedTests: XCTestCase { let derived = store .normalizedStorage(.keyPath(\.db)) .table(.keyPath(\.book)) - .derivedEntity(entityID: Book.EntityID.init("1")) + .derived(from: Book.EntityID.init("1")) var received: [Book?] = [] @@ -56,17 +56,17 @@ final class VergeNormalizationDerivedTests: XCTestCase { let derived1 = store .normalizedStorage(.keyPath(\.db)) .table(.keyPath(\.book)) - .derivedEntity(entityID: Book.EntityID.init("1")) + .derived(from: Book.EntityID.init("1")) let derived2 = store .normalizedStorage(.keyPath(\.db)) .table(.keyPath(\.book)) - .derivedEntity(entityID: Book.EntityID.init("1")) + .derived(from: Book.EntityID.init("1")) let derived3 = store .normalizedStorage(.keyPath(\.db)) .table(.keyPath(\.book2)) - .derivedEntity(entityID: Book.EntityID.init("1")) + .derived(from: Book.EntityID.init("1")) XCTAssert(derived1 === derived2) XCTAssert(derived2 !== derived3) From 56040d3e45815a9d72ed953fccf43e81a685fc70 Mon Sep 17 00:00:00 2001 From: Muukii Date: Fri, 6 Oct 2023 11:52:08 +0900 Subject: [PATCH 14/16] :evergreen_tree: Update --- Sources/VergeNormalization/NormalizedStorageType.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/VergeNormalization/NormalizedStorageType.swift b/Sources/VergeNormalization/NormalizedStorageType.swift index a0cc080887..be5cfd32e3 100644 --- a/Sources/VergeNormalization/NormalizedStorageType.swift +++ b/Sources/VergeNormalization/NormalizedStorageType.swift @@ -1,6 +1,9 @@ public protocol NormalizedStorageType: Equatable { + /** + Performs any additional operations for updating. + */ func finalizeTransaction(transaction: inout ModifyingTransaction) static func compare(lhs: Self, rhs: Self) -> Bool From c572138484bba99c89bc2b40924870087324870e Mon Sep 17 00:00:00 2001 From: Muukii Date: Tue, 10 Oct 2023 13:29:41 +0900 Subject: [PATCH 15/16] areEqual --- Sources/Verge/Store/Changes.swift | 1 + Sources/{Verge/Library => VergeComparator}/PackedCompare.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) rename Sources/{Verge/Library => VergeComparator}/PackedCompare.swift (71%) diff --git a/Sources/Verge/Store/Changes.swift b/Sources/Verge/Store/Changes.swift index 89aaca64c7..76cf7c7afe 100644 --- a/Sources/Verge/Store/Changes.swift +++ b/Sources/Verge/Store/Changes.swift @@ -20,6 +20,7 @@ // THE SOFTWARE. import Foundation +@_spi(Internal) import VergeComparator #if !COCOAPODS #endif diff --git a/Sources/Verge/Library/PackedCompare.swift b/Sources/VergeComparator/PackedCompare.swift similarity index 71% rename from Sources/Verge/Library/PackedCompare.swift rename to Sources/VergeComparator/PackedCompare.swift index e6887acf19..8d739e830d 100644 --- a/Sources/Verge/Library/PackedCompare.swift +++ b/Sources/VergeComparator/PackedCompare.swift @@ -1,5 +1,6 @@ -func areEqual(_ lhs: (repeat each Element), _ rhs: (repeat each Element)) -> Bool { +@_spi(Internal) +public func areEqual(_ lhs: (repeat each Element), _ rhs: (repeat each Element)) -> Bool { // https://github.com/apple/swift-evolution/blob/main/proposals/0408-pack-iteration.md From 3ea3d9714503dbfc6aa68d8a5425104a6bfbfe06 Mon Sep 17 00:00:00 2001 From: Muukii Date: Tue, 10 Oct 2023 14:53:42 +0900 Subject: [PATCH 16/16] :evergreen_tree: Update --- .../VergeMacrosTests/DatabaseMacroTests.swift | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/Tests/VergeMacrosTests/DatabaseMacroTests.swift b/Tests/VergeMacrosTests/DatabaseMacroTests.swift index 20a3a982bf..723468444d 100644 --- a/Tests/VergeMacrosTests/DatabaseMacroTests.swift +++ b/Tests/VergeMacrosTests/DatabaseMacroTests.swift @@ -19,14 +19,40 @@ final class DatabaseMacroTests: XCTestCase { struct MyDatabase { @TableAccessor let user: String - @TableAccessor(hoge) + @TableAccessor(hoge) let user: String } """#, expandedSource: #""" struct MyDatabase { - @Table + @TableAccessor let user: String + @TableAccessor(hoge) + let user: String + + @TableAccessor var _$user: String + + @TableAccessor(hoge) var _$user: String + } + + extension MyDatabase { + static func compare(lhs: Self, rhs: Self) -> Bool { + + return true + } + } + + extension MyDatabase { + + } + + extension MyDatabase: NormalizedStorageType { + } + + extension MyDatabase: Equatable { + } + + extension MyDatabase { } """#, macros: macros @@ -45,8 +71,29 @@ final class DatabaseMacroTests: XCTestCase { """#, expandedSource: #""" struct MyDatabase { - @Table let user: String + + var _$user: String + } + + extension MyDatabase { + static func compare(lhs: Self, rhs: Self) -> Bool { + + return true + } + } + + extension MyDatabase { + + } + + extension MyDatabase: NormalizedStorageType { + } + + extension MyDatabase: Equatable { + } + + extension MyDatabase { } """#, macros: macros