├── Package.swift ├── Sources └── PerfectCRUD │ ├── Expression │ ├── Logical.swift │ ├── Like.swift │ ├── In.swift │ ├── Comparison.swift │ ├── Equality.swift │ ├── InInts.swift │ ├── Expression.swift │ ├── ComparisonInts.swift │ └── EqualityInts.swift │ ├── Delete.swift │ ├── Database.swift │ ├── Where.swift │ ├── Coding │ ├── Coding.swift │ ├── CodingJoins.swift │ ├── CodingBindings.swift │ ├── CodingRows.swift │ ├── CodingNames.swift │ └── CodingKeyPaths.swift │ ├── Update.swift │ ├── Select.swift │ ├── Table.swift │ ├── Insert.swift │ ├── Logging.swift │ ├── Join.swift │ ├── Create.swift │ └── PerfectCRUD.swift ├── .gitignore ├── LICENSE └── README.zh_CN.md /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // Generated automatically by Perfect Assistant 2 3 | // Date: 2017-11-22 17:52:51 +0000 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PerfectCRUD", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | ], 11 | products: [ 12 | .library(name: "PerfectCRUD", targets: ["PerfectCRUD"]) 13 | ], 14 | dependencies: [ 15 | 16 | ], 17 | targets: [ 18 | .target(name: "PerfectCRUD", dependencies: []) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Expression/Logical.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logical.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2018-02-18. 6 | // 7 | 8 | import Foundation 9 | 10 | // && 11 | public func && (lhs: CRUDBooleanExpression, rhs: CRUDBooleanExpression) -> CRUDBooleanExpression { 12 | return RealBooleanExpression(.and(lhs: lhs.crudExpression, rhs: rhs.crudExpression)) 13 | } 14 | // || 15 | public func || (lhs: CRUDBooleanExpression, rhs: CRUDBooleanExpression) -> CRUDBooleanExpression { 16 | return RealBooleanExpression(.or(lhs: lhs.crudExpression, rhs: rhs.crudExpression)) 17 | } 18 | // ! 19 | public prefix func ! (rhs: CRUDBooleanExpression) -> CRUDBooleanExpression { 20 | return RealBooleanExpression(.not(rhs: rhs.crudExpression)) 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Delete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectCRUDDelete.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2017-12-03. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Deleteable: TableProtocol { 11 | @discardableResult 12 | func delete() throws -> Delete 13 | } 14 | 15 | public extension Deleteable { 16 | @discardableResult 17 | func delete() throws -> Delete { 18 | return try .init(fromTable: self) 19 | } 20 | } 21 | 22 | public struct Delete: FromTableProtocol, CommandProtocol { 23 | public typealias FromTableType = A 24 | public typealias OverAllForm = OAF 25 | public let fromTable: FromTableType 26 | public let sqlGenState: SQLGenState 27 | init(fromTable ft: FromTableType) throws { 28 | fromTable = ft 29 | let delegate = ft.databaseConfiguration.sqlGenDelegate 30 | var state = SQLGenState(delegate: delegate) 31 | state.command = .delete 32 | try ft.setState(state: &state) 33 | try ft.setSQL(state: &state) 34 | sqlGenState = state 35 | for stat in state.statements { 36 | let exeDelegate = try databaseConfiguration.sqlExeDelegate(forSQL: stat.sql) 37 | try exeDelegate.bind(stat.bindings) 38 | _ = try exeDelegate.hasNext() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Database.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectCRUDDatabase.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2017-12-02. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Database: DatabaseProtocol { 11 | public typealias Configuration = C 12 | public let configuration: Configuration 13 | public init(configuration c: Configuration) { 14 | configuration = c 15 | } 16 | public func table(_ form: T.Type) -> Table { 17 | return .init(database: self) 18 | } 19 | } 20 | 21 | public extension Database { 22 | func sql(_ sql: String, bindings: Bindings = []) throws { 23 | CRUDLogging.log(.query, sql) 24 | let delegate = try configuration.sqlExeDelegate(forSQL: sql) 25 | try delegate.bind(bindings, skip: 0) 26 | _ = try delegate.hasNext() 27 | } 28 | func sql(_ sql: String, bindings: Bindings = [], _ type: A.Type) throws -> [A] { 29 | CRUDLogging.log(.query, sql) 30 | let delegate = try configuration.sqlExeDelegate(forSQL: sql) 31 | try delegate.bind(bindings, skip: 0) 32 | var ret: [A] = [] 33 | while try delegate.hasNext() { 34 | let rowDecoder: CRUDRowDecoder = CRUDRowDecoder(delegate: delegate) 35 | ret.append(try A(from: rowDecoder)) 36 | } 37 | return ret 38 | } 39 | } 40 | 41 | public extension Database { 42 | func transaction(_ body: () throws -> T) throws -> T { 43 | try sql("BEGIN") 44 | do { 45 | let r = try body() 46 | try sql("COMMIT") 47 | return r 48 | } catch { 49 | try sql("ROLLBACK") 50 | throw error 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Where.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectCRUDWhere.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2018-01-03. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public struct Where: TableProtocol, FromTableProtocol, Selectable { 12 | public typealias Form = OAF 13 | public typealias FromTableType = A 14 | public typealias OverAllForm = OAF 15 | public let fromTable: FromTableType 16 | let expression: Expression 17 | public func setState(state: inout SQLGenState) throws { 18 | try fromTable.setState(state: &state) 19 | state.whereExpr = expression 20 | } 21 | public func setSQL(state: inout SQLGenState) throws { 22 | try fromTable.setSQL(state: &state) 23 | } 24 | } 25 | 26 | public extension Where where OverAllForm == FromTableType.Form { 27 | @discardableResult 28 | func delete() throws -> Delete { 29 | return try .init(fromTable: self) 30 | } 31 | @discardableResult 32 | func update(_ instance: OverAllForm) throws -> Update { 33 | return try .init(fromTable: self, instance: instance, includeKeys: [], excludeKeys: []) 34 | } 35 | @discardableResult 36 | func update(_ instance: OverAllForm, setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> Update { 37 | return try .init(fromTable: self, instance: instance, includeKeys: [setKeys] + rest, excludeKeys: []) 38 | } 39 | @discardableResult 40 | func update(_ instance: OverAllForm, ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> Update { 41 | return try .init(fromTable: self, instance: instance, includeKeys: [], excludeKeys: [ignoreKeys] + rest) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | Package.resolved 69 | .DS_Store 70 | *.xcodeproj/ 71 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Coding/Coding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectCRUDCoding.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2017-11-22. 6 | // Copyright (C) 2017 PerfectlySoft, Inc. 7 | // 8 | //===----------------------------------------------------------------------===// 9 | // 10 | // This source file is part of the Perfect.org open source project 11 | // 12 | // Copyright (c) 2015 - 2017 PerfectlySoft Inc. and the Perfect project authors 13 | // Licensed under Apache License v2.0 14 | // 15 | // See http://perfect.org/licensing.html for license information 16 | // 17 | //===----------------------------------------------------------------------===// 18 | // 19 | 20 | import Foundation 21 | 22 | public struct CRUDDecoderError: Error { 23 | public let msg: String 24 | public init(_ m: String) { 25 | msg = m 26 | CRUDLogging.log(.error, m) 27 | } 28 | } 29 | 30 | public struct CRUDEncoderError: Error { 31 | public let msg: String 32 | public init(_ m: String) { 33 | msg = m 34 | CRUDLogging.log(.error, m) 35 | } 36 | } 37 | 38 | public struct ColumnKey : CodingKey { 39 | public var stringValue: String 40 | public var intValue: Int? = nil 41 | public init?(stringValue s: String) { 42 | stringValue = s 43 | } 44 | public init?(intValue: Int) { 45 | return nil 46 | } 47 | } 48 | 49 | public indirect enum SpecialType { 50 | case uint8Array, int8Array, data, uuid, date, codable, url, wrapped 51 | public init?(_ type: Any.Type) { 52 | switch type { 53 | case is WrappedCodableProvider.Type: 54 | self = .wrapped 55 | case is [Int8].Type: 56 | self = .int8Array 57 | case is [UInt8].Type: 58 | self = .uint8Array 59 | case is Data.Type: 60 | self = .data 61 | case is UUID.Type: 62 | self = .uuid 63 | case is Date.Type: 64 | self = .date 65 | case is URL.Type: 66 | self = .url 67 | case is Codable.Type: 68 | self = .codable 69 | default: 70 | return nil 71 | } 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Expression/Like.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Like.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2018-02-18. 6 | // 7 | 8 | import Foundation 9 | 10 | // %=% LIKE 11 | public func %=% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 12 | return RealBooleanExpression(.like(lhs: .keyPath(lhs), wild1: true, rhs, wild2: true)) 13 | } 14 | public func %=% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 15 | return RealBooleanExpression(.like(lhs: .keyPath(lhs), wild1: true, rhs, wild2: true)) 16 | } 17 | // *~ LIKE v% 18 | public func =% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 19 | return RealBooleanExpression(.like(lhs: .keyPath(lhs), wild1: false, rhs, wild2: true)) 20 | } 21 | public func =% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 22 | return RealBooleanExpression(.like(lhs: .keyPath(lhs), wild1: false, rhs, wild2: true)) 23 | } 24 | // ~* LIKE %v 25 | public func %= (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 26 | return RealBooleanExpression(.like(lhs: .keyPath(lhs), wild1: true, rhs, wild2: false)) 27 | } 28 | public func %= (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 29 | return RealBooleanExpression(.like(lhs: .keyPath(lhs), wild1: true, rhs, wild2: false)) 30 | } 31 | // !~ NOT LIKE 32 | public func %!=% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 33 | return !(lhs %=% rhs) 34 | } 35 | public func %!=% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 36 | return !(lhs %=% rhs) 37 | } 38 | public func !=% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 39 | return !(lhs =% rhs) 40 | } 41 | public func !=% (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 42 | return !(lhs =% rhs) 43 | } 44 | public func %!= (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 45 | return !(lhs %= rhs) 46 | } 47 | public func %!= (lhs: KeyPath, rhs: String) -> CRUDBooleanExpression { 48 | return !(lhs %= rhs) 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Expression/In.swift: -------------------------------------------------------------------------------- 1 | // 2 | // In.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2018-02-18. 6 | // 7 | 8 | import Foundation 9 | 10 | // ~ IN 11 | public func ~ (lhs: KeyPath, rhs: [String]) -> CRUDBooleanExpression { 12 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .string($0) })) 13 | } 14 | public func ~ (lhs: KeyPath, rhs: [Double]) -> CRUDBooleanExpression { 15 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .decimal($0) })) 16 | } 17 | public func ~ (lhs: KeyPath, rhs: [UUID]) -> CRUDBooleanExpression { 18 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .uuid($0) })) 19 | } 20 | public func ~ (lhs: KeyPath, rhs: [Date]) -> CRUDBooleanExpression { 21 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .date($0) })) 22 | } 23 | public func ~ (lhs: KeyPath, rhs: [String]) -> CRUDBooleanExpression { 24 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .string($0) })) 25 | } 26 | public func ~ (lhs: KeyPath, rhs: [Double]) -> CRUDBooleanExpression { 27 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .decimal($0) })) 28 | } 29 | public func ~ (lhs: KeyPath, rhs: [UUID]) -> CRUDBooleanExpression { 30 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .uuid($0) })) 31 | } 32 | public func ~ (lhs: KeyPath, rhs: [Date]) -> CRUDBooleanExpression { 33 | return RealBooleanExpression(.in(lhs: .keyPath(lhs), rhs: rhs.map { .date($0) })) 34 | } 35 | // !~ NOT IN 36 | public func !~ (lhs: KeyPath, rhs: [String]) -> CRUDBooleanExpression { 37 | return !(lhs ~ rhs) 38 | } 39 | public func !~ (lhs: KeyPath, rhs: [Double]) -> CRUDBooleanExpression { 40 | return !(lhs ~ rhs) 41 | } 42 | public func !~ (lhs: KeyPath, rhs: [UUID]) -> CRUDBooleanExpression { 43 | return !(lhs ~ rhs) 44 | } 45 | public func !~ (lhs: KeyPath, rhs: [Date]) -> CRUDBooleanExpression { 46 | return !(lhs ~ rhs) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Update.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectCRUDUpdate.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2017-12-02. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Updatable: TableProtocol { 11 | @discardableResult 12 | func update(_ instance: OverAllForm, setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> Update 13 | @discardableResult 14 | func update(_ instance: OverAllForm, ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> Update 15 | @discardableResult 16 | func update(_ instance: OverAllForm) throws -> Update 17 | } 18 | 19 | public extension Updatable { 20 | @discardableResult 21 | func update(_ instance: OverAllForm) throws -> Update { 22 | return try .init(fromTable: self, instance: instance, includeKeys: [], excludeKeys: []) 23 | } 24 | @discardableResult 25 | func update(_ instance: OverAllForm, setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> Update { 26 | return try .init(fromTable: self, instance: instance, includeKeys: [setKeys] + rest, excludeKeys: []) 27 | } 28 | @discardableResult 29 | func update(_ instance: OverAllForm, ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> Update { 30 | return try .init(fromTable: self, instance: instance, includeKeys: [], excludeKeys: [ignoreKeys] + rest) 31 | } 32 | } 33 | 34 | public struct Update: FromTableProtocol, CommandProtocol { 35 | public typealias FromTableType = A 36 | public typealias OverAllForm = OAF 37 | public let fromTable: FromTableType 38 | public let sqlGenState: SQLGenState 39 | init(fromTable ft: FromTableType, 40 | instance: OAF, 41 | includeKeys: [PartialKeyPath], 42 | excludeKeys: [PartialKeyPath]) throws { 43 | fromTable = ft 44 | let delegate = ft.databaseConfiguration.sqlGenDelegate 45 | var state = SQLGenState(delegate: delegate) 46 | state.command = .update 47 | try ft.setState(state: &state) 48 | let td = state.tableData[0] 49 | let kpDecoder = td.keyPathDecoder 50 | guard let kpInstance = td.modelInstance else { 51 | throw CRUDSQLGenError("Could not get model instance for key path decoder \(OAF.self)") 52 | } 53 | let includeNames: [String] 54 | if includeKeys.isEmpty { 55 | let columnDecoder = CRUDColumnNameDecoder() 56 | _ = try OverAllForm.init(from: columnDecoder) 57 | includeNames = columnDecoder.collectedKeys.map { $0.name } 58 | } else { 59 | includeNames = try includeKeys.map { 60 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else { 61 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)") 62 | } 63 | return n 64 | } 65 | } 66 | let excludeNames: [String] = try excludeKeys.map { 67 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else { 68 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)") 69 | } 70 | return n 71 | } 72 | let encoder = try CRUDBindingsEncoder(delegate: delegate) 73 | try instance.encode(to: encoder) 74 | state.bindingsEncoder = encoder 75 | state.columnFilters = (include: includeNames, exclude: excludeNames) 76 | try ft.setSQL(state: &state) 77 | sqlGenState = state 78 | if let stat = state.statements.first { // multi statements?! 79 | let exeDelegate = try databaseConfiguration.sqlExeDelegate(forSQL: stat.sql) 80 | try exeDelegate.bind(stat.bindings) 81 | _ = try exeDelegate.hasNext() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/PerfectCRUD/Select.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfectCRUDSelect.swift 3 | // PerfectCRUD 4 | // 5 | // Created by Kyle Jessup on 2017-12-02. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SelectIterator: IteratorProtocol { 11 | public typealias Element = A.OverAllForm 12 | let select: A? 13 | let exeDelegate: SQLExeDelegate? 14 | init(select s: A) throws { 15 | select = s 16 | exeDelegate = try SQLTopExeDelegate(genState: s.sqlGenState, configurator: s.fromTable.databaseConfiguration) 17 | } 18 | init() { 19 | select = nil 20 | exeDelegate = nil 21 | } 22 | public mutating func next() -> Element? { 23 | guard let delegate = exeDelegate else { 24 | return nil 25 | } 26 | do { 27 | if try delegate.hasNext() { 28 | let rowDecoder: CRUDRowDecoder = CRUDRowDecoder(delegate: delegate) 29 | return try Element(from: rowDecoder) 30 | } 31 | } catch { 32 | CRUDLogging.log(.error, "Error thrown in SelectIterator.next(). Caught: \(error)") 33 | } 34 | return nil 35 | } 36 | public mutating func nextElement() throws -> Element? { 37 | guard let delegate = exeDelegate else { 38 | return nil 39 | } 40 | if try delegate.hasNext() { 41 | let rowDecoder: CRUDRowDecoder = CRUDRowDecoder(delegate: delegate) 42 | return try Element(from: rowDecoder) 43 | } 44 | return nil 45 | } 46 | } 47 | 48 | public struct Select: SelectProtocol { 49 | public typealias Iterator = SelectIterator