├── Cartfile ├── Cartfile.resolved ├── Amigo.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── Amigo.xcscmblueprint └── xcshareddata │ └── xcschemes │ └── Amigo.xcscheme ├── Amigo ├── MetaItem.swift ├── SchemaItem.swift ├── EngineFactory.swift ├── DefaultMapper.swift ├── Models │ └── SQLParams.swift ├── AmigoEntityDescriptionMapper.swift ├── Update.swift ├── Delete.swift ├── typeToTableName.swift ├── Insert.swift ├── Filterable.swift ├── SQLiteFormat.h ├── Amigo.h ├── MetaData.swift ├── AmigoBatchOperation.swift ├── Mapper.swift ├── SQLiteEngineFactory.swift ├── AmigoModel.swift ├── OrderBy.swift ├── Compiler.swift ├── MetaModel.swift ├── Engine.swift ├── ForeignKey.swift ├── SQLiteEngine+Schema.swift ├── Info.plist ├── Expressions.swift ├── AmigoSessionAsyncAction.swift ├── ThreadLocalEngineFactory.swift ├── SQLiteTypeCompiler.swift ├── Index.swift ├── AmigoConfiguration.swift ├── TypeCompiler.swift ├── SQLiteFormat.m ├── Table.swift ├── SQLiteEngine+QuerySet.swift ├── SQLiteEngine+Master.swift ├── SQLiteEngine+Transactions.swift ├── NSManagedObjectModel+Sorted.swift ├── Relationship.swift ├── EntityDescriptionMapper.swift ├── ORMModel.swift ├── AmigoSessionModelAction.swift ├── AmigoMetaData.swift ├── Column.swift ├── SQLiteEngine.swift ├── Fields.swift ├── Select.swift ├── Amigo.swift └── QuerySet.swift ├── docs ├── models │ ├── index.rst │ └── mom.rst ├── setup │ ├── engine.rst │ ├── index.rst │ └── orm_models.rst ├── index.rst ├── sessions │ └── index.rst ├── querying │ └── index.rst ├── Makefile └── make.bat ├── AmigoTests ├── Info.plist ├── AmigoTests.swift ├── AmigoTestBase.swift ├── EntityDescriptionMapperTests.swift ├── SelectTests.swift ├── Models.swift ├── SQLiteFormatTests.swift ├── AmigoMeta.swift ├── SQLiteEngineTests.swift ├── MOMExtensionTests.swift ├── AmigoSessionAsyncTests.swift ├── MetaTableTests.swift ├── PerfTests.swift ├── MetaColumnTests.swift ├── ModelMappingTests.swift ├── AmigoQuerySetTests.swift └── App.xcdatamodeld │ └── App.xcdatamodel │ └── contents ├── .gitignore ├── LICENSE └── README.md /Cartfile: -------------------------------------------------------------------------------- 1 | github "ccgus/fmdb" ~> 2.6 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ccgus/fmdb" "2.6" 2 | -------------------------------------------------------------------------------- /Amigo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Amigo/MetaItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaItem.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/25/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | public protocol MetaItem{ 11 | 12 | } -------------------------------------------------------------------------------- /Amigo/SchemaItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SchemaItem.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | public protocol SchemaItem: MetaItem{ 11 | 12 | } -------------------------------------------------------------------------------- /Amigo/EngineFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EngineFactory.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/22/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol EngineFactory{ 12 | func connect() -> Engine 13 | } -------------------------------------------------------------------------------- /Amigo/DefaultMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultMapper.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/21/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class DefaultMapper: Mapper{ 12 | public let metadata = MetaData() 13 | 14 | public required init(){} 15 | } -------------------------------------------------------------------------------- /Amigo/Models/SQLParams.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLParams.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/13/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SQLParams{ 12 | let queryParams: [AnyObject] 13 | let defaultValues: [String: AnyObject] 14 | let automaticPrimaryKey: Bool 15 | } -------------------------------------------------------------------------------- /Amigo/AmigoEntityDescriptionMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoEntityDescriptionMapper.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/21/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | public protocol AmigoEntityDescriptionMapper: Mapper{ 13 | func map(entity: NSEntityDescription) -> ORMModel 14 | } 15 | -------------------------------------------------------------------------------- /Amigo/Update.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Update.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/23/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Update: Filterable{ 12 | public let table: Table 13 | public var predicate: String? 14 | public var predicateParams: [AnyObject]? 15 | 16 | public init(_ table: Table){ 17 | self.table = table 18 | } 19 | } -------------------------------------------------------------------------------- /Amigo/Delete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delete.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/21/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public class Delete: Filterable { 13 | public let table: Table 14 | public var predicate: String? 15 | public var predicateParams: [AnyObject]? 16 | 17 | public init(_ table: Table){ 18 | self.table = table 19 | } 20 | } -------------------------------------------------------------------------------- /docs/models/index.rst: -------------------------------------------------------------------------------- 1 | Models 2 | ================================= 3 | 4 | Amigo works by mapping your data models derived from a 5 | :code:`Amigo.AmigoModel` into an :code:`Amigo.ORMModel` 6 | 7 | There are 2 way you can perform this mapping. 8 | 9 | 1. Use a Managed Object Model 10 | 2. Manual Mapping 11 | 12 | The Managed Object Model mapping just automates the Manual Mapping 13 | process. 14 | 15 | Contents: 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | mom 21 | orm_models 22 | -------------------------------------------------------------------------------- /Amigo/typeToTableName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // typeToTableName.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 8/2/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | func typeToTableName(type: T.Type) -> String{ 13 | return typeToTableName(type.description()) 14 | } 15 | 16 | func typeToTableName(type: String) -> String{ 17 | let parts = type.unicodeScalars.split{ $0 == "." }.map{ String($0).lowercaseString } 18 | return parts.joinWithSeparator("_") 19 | } -------------------------------------------------------------------------------- /Amigo/Insert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Insert.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/19/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public class Insert { 13 | public let table: Table 14 | public let upsert: Bool 15 | 16 | public init(_ table: Table){ 17 | self.table = table 18 | self.upsert = false 19 | } 20 | 21 | public init(_ table: Table, upsert: Bool = false){ 22 | self.table = table 23 | self.upsert = upsert 24 | } 25 | } -------------------------------------------------------------------------------- /Amigo/Filterable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Filterable.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/21/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Filterable{ 12 | var predicate: String? {get set} 13 | var predicateParams: [AnyObject]? {get set} 14 | } 15 | 16 | 17 | extension Filterable{ 18 | 19 | mutating func filter(value: String, params:[AnyObject]? = nil) -> Self{ 20 | predicate = value 21 | predicateParams = params 22 | 23 | return self 24 | } 25 | } -------------------------------------------------------------------------------- /Amigo/SQLiteFormat.h: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteFormat.h 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/14/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | #pragma once 10 | 11 | @interface SQLiteFormat : NSObject 12 | + (nonnull NSString *)format:(nullable char *)format, ...; 13 | + (nonnull NSString *)escapeWithQuotes:(nullable NSString *)value; 14 | + (nonnull NSString *)escapeWithoutQuotes:(nullable NSString *)value; 15 | + (nonnull NSString *)escapeBlob:(nullable NSData *)value; 16 | + (nonnull NSString *)hexStringWithData:(nonnull NSData *)data; 17 | @end 18 | 19 | -------------------------------------------------------------------------------- /Amigo/Amigo.h: -------------------------------------------------------------------------------- 1 | // 2 | // Amigo.h 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 6/29/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | //! Project version number for Amigo. 13 | FOUNDATION_EXPORT double AmigoVersionNumber; 14 | 15 | //! Project version string for Amigo. 16 | FOUNDATION_EXPORT const unsigned char AmigoVersionString[]; 17 | 18 | // In this header, you should import all the public headers of your framework using statements like #import 19 | 20 | #import 21 | 22 | -------------------------------------------------------------------------------- /Amigo/MetaData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaData.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/5/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class MetaData{ 12 | public var tables = [String: Table]() 13 | var _sortedTables = [Table]() 14 | 15 | public var sortedTables:[Table]{ 16 | return _sortedTables 17 | } 18 | 19 | public init(){ 20 | 21 | } 22 | 23 | public func createAll(engine: Engine){ 24 | let sql = engine.generateSchema(self) 25 | engine.createAll(sql) 26 | } 27 | } -------------------------------------------------------------------------------- /Amigo/AmigoBatchOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BatchOperation.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/14/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AmigoBatchOperation{ 12 | init(session: AmigoSession) 13 | 14 | func add(obj: T) 15 | func add(obj: T, upsert: Bool) 16 | func add(obj: [T], upsert: Bool) 17 | func add(obj: [T]) 18 | 19 | func delete(obj: T) 20 | func delete(obj: [T]) 21 | 22 | func execute() 23 | } -------------------------------------------------------------------------------- /Amigo/Mapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mapper.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/12/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public protocol Mapper{ 13 | init() 14 | } 15 | 16 | extension Mapper{ 17 | func instanceFromString(value: String) -> AmigoModel{ 18 | let type = NSClassFromString(value) as! NSObject.Type 19 | let obj = type.init() as! AmigoModel 20 | return obj 21 | } 22 | 23 | func dottedNameToTableName(name: String) -> String{ 24 | return typeToTableName(name) 25 | } 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /Amigo/SQLiteEngineFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngineFactory.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/22/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SQLiteEngineFactory: EngineFactory{ 12 | public let path: String 13 | public let echo: Bool 14 | public let engine: SQLiteEngine 15 | 16 | public init(_ path: String, echo: Bool = false){ 17 | self.path = path 18 | self.echo = echo 19 | self.engine = SQLiteEngine(path, echo: echo) 20 | } 21 | 22 | public func connect() -> Engine { 23 | return engine 24 | } 25 | } -------------------------------------------------------------------------------- /Amigo/AmigoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoModel.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/2/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | 13 | @objc 14 | public class AmigoModel: NSObject{ 15 | static var amigoModelIndex = [String: ORMModel]() 16 | 17 | public override init(){ 18 | super.init() 19 | let _ = amigoModel 20 | } 21 | 22 | lazy var qualifiedName: String = { 23 | return self.dynamicType.description() 24 | }() 25 | 26 | lazy var amigoModel: ORMModel = { 27 | return AmigoModel.amigoModelIndex[self.qualifiedName]! 28 | }() 29 | } -------------------------------------------------------------------------------- /Amigo/OrderBy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderBy.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/28/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol OrderBy{ 12 | var keyPath: String {get} 13 | var ascending: Bool {get} 14 | 15 | init(_ keyPath: String) 16 | } 17 | 18 | public struct Asc: OrderBy{ 19 | public let ascending = true 20 | public let keyPath: String 21 | 22 | public init(_ keyPath: String){ 23 | self.keyPath = keyPath 24 | } 25 | } 26 | 27 | public struct Desc: OrderBy{ 28 | public let ascending = false 29 | public let keyPath: String 30 | 31 | public init(_ keyPath: String){ 32 | self.keyPath = keyPath 33 | } 34 | } -------------------------------------------------------------------------------- /Amigo/Compiler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Compiler.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Compiler{ 12 | func compile(expression: CreateTable) -> String 13 | func compile(expression: CreateColumn) -> String 14 | func compile(expression: CreateIndex) -> String 15 | func compile(expression: Join) -> String 16 | func compile(expression: Select) -> String 17 | func compile(expression: Insert) -> String 18 | func compile(expression: Update) -> String 19 | func compile(expression: Delete) -> String 20 | func compile(expression: NSPredicate, table: Table, models: [String: ORMModel]) -> (String, [AnyObject]) 21 | } -------------------------------------------------------------------------------- /Amigo/MetaModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaModel.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/3/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //public struct MetaModel{ 12 | // 13 | // public let table: Table 14 | // public let primaryKey: Column 15 | // public let columns: [Column] 16 | // public let foreignKeys: [ForeignKey]? 17 | // 18 | // // public List ManyToMany { get; set; } 19 | // // public List ModelTypes { get; set; } 20 | // 21 | // init(table: Table, columns:[Column], primaryKey: Column, foreignKeys:[ForeignKey]? = nil){ 22 | // self.table = table 23 | // self.columns = columns 24 | // self.primaryKey = primaryKey 25 | // self.foreignKeys = foreignKeys 26 | // } 27 | // 28 | //} 29 | -------------------------------------------------------------------------------- /AmigoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | docs/_build 3 | docs/_static 4 | docs/_templates 5 | 6 | # Xcode 7 | # 8 | build/ 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | *.xccheckout 19 | *.moved-aside 20 | DerivedData 21 | *.hmap 22 | *.ipa 23 | *.xcuserstate 24 | 25 | # CocoaPods 26 | # 27 | # We recommend against adding the Pods directory to your .gitignore. However 28 | # you should judge for yourself, the pros and cons are mentioned at: 29 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 30 | # 31 | # Pods/ 32 | 33 | # Carthage 34 | # 35 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 36 | 37 | Carthage/Checkouts 38 | Carthage/Build 39 | -------------------------------------------------------------------------------- /Amigo/Engine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Engine.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/2/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | 10 | public protocol Engine { 11 | 12 | var compiler: Compiler {get} 13 | var fetchLastRowIdAfterInsert: Bool {get} 14 | 15 | 16 | func createAll(sql: String) 17 | func generateSchema(meta: MetaData) -> String 18 | func lastrowid() -> Int 19 | 20 | func execute(sql: String, params: [AnyObject]!, mapper: Input -> Output) -> Output 21 | func execute(sql: String, params: [AnyObject]!) 22 | func execute(sql: String) 23 | func execute(queryset: QuerySet) -> [T] 24 | func createBatchOperation(session: AmigoSession) -> AmigoBatchOperation 25 | 26 | func beginTransaction() 27 | func commitTransaction() 28 | func rollback() 29 | } -------------------------------------------------------------------------------- /Amigo/ForeignKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForeignKey.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/3/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ForeignKey{ 12 | public let relatedColumn: Column 13 | public var column: Column! 14 | 15 | public init(_ relatedColumn: Column, column: Column){ 16 | self.relatedColumn = relatedColumn 17 | self.column = column 18 | } 19 | 20 | public init(_ relatedColumn: Column){ 21 | self.relatedColumn = relatedColumn 22 | } 23 | 24 | public init(_ table: Table){ 25 | self.relatedColumn = table.primaryKey! 26 | } 27 | 28 | public init(_ model: ORMModel){ 29 | self.relatedColumn = model.table.primaryKey! 30 | } 31 | 32 | public var relatedTable: Table{ 33 | return relatedColumn.table! 34 | } 35 | } -------------------------------------------------------------------------------- /Amigo/SQLiteEngine+Schema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine+Schema.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/6/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | 13 | extension SQLiteEngine{ 14 | 15 | public func generateSchema(meta: MetaData) -> String{ 16 | let tables = meta.sortedTables.map{ 17 | compiler.compile(CreateTable($0)) 18 | } 19 | 20 | return tables.joinWithSeparator("\n") 21 | } 22 | 23 | public func createAll(sql: String){ 24 | 25 | db.inTransaction { (db, rollback) -> Void in 26 | 27 | if self.echo{ 28 | self.echo(sql) 29 | } 30 | 31 | // needs to be executeStatements, 32 | // AKA Multiple SQL Commands 33 | db.executeStatements(sql) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Amigo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.3.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Amigo/Expressions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateTable.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/6/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Expression{ 12 | let element: T 13 | 14 | public init(element: T){ 15 | self.element = element 16 | } 17 | } 18 | 19 | 20 | public class CreateColumn: Expression{ 21 | public init(_ column: Column){ 22 | super.init(element: column) 23 | } 24 | } 25 | 26 | 27 | public class CreateIndex: Expression{ 28 | public init(_ index: Index){ 29 | super.init(element: index) 30 | } 31 | } 32 | 33 | 34 | public class CreateTable: Expression{ 35 | let columns: [CreateColumn] 36 | 37 | public init(_ table: Table){ 38 | self.columns = table.sortedColumns.map(CreateColumn.init) 39 | super.init(element: table) 40 | } 41 | } -------------------------------------------------------------------------------- /Amigo/AmigoSessionAsyncAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoSessionAsyncAction.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/16/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public class AmigoSessionAsyncAction{ 13 | 14 | var action: (() -> T)! 15 | var queue: dispatch_queue_t! 16 | 17 | public init(action: () -> T, queue: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)){ 18 | self.action = action 19 | self.queue = queue 20 | } 21 | 22 | 23 | public func run(){ 24 | dispatch_async(queue){ 25 | self.action() 26 | } 27 | } 28 | 29 | public func then(complete: (T) -> ()){ 30 | dispatch_async(queue){ 31 | let results = self.action() 32 | 33 | dispatch_async(dispatch_get_main_queue()){ 34 | complete(results) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AmigoTests/AmigoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoTests.swift 3 | // AmigoTests 4 | // 5 | // Created by Adam Venturella on 6/29/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import FMDB 12 | import Amigo 13 | 14 | class AmigoTests: AmigoTestBase { 15 | 16 | func testSelectRelated(){ 17 | let select: Select 18 | let sql: String 19 | let query = amigo.query(People) 20 | query.selectRelated("dog", "cat") 21 | 22 | select = query.compile() 23 | sql = amigo.config.engine.compiler.compile(select) 24 | print(sql) 25 | } 26 | 27 | func testSelectValues(){ 28 | let select: Select 29 | let sql: String 30 | let query = amigo.query(People) 31 | query.selectRelated("dog") 32 | query.values("id", "label", "dog.id") 33 | 34 | select = query.compile() 35 | sql = amigo.config.engine.compiler.compile(select) 36 | print(sql) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/models/mom.rst: -------------------------------------------------------------------------------- 1 | Managed Object Model 2 | =================================== 3 | 4 | 5 | Yup, Amigo can turn :code:`NSEntityDescriptions` along with their 6 | relationships into your tables for you. There are only a couple things to know. 7 | 8 | 9 | 1. Unlike :code:`CoreData`, you need to specify your primary key field. 10 | This could totally be automated for you, we havent decided if we like 11 | that or not yet. You do this by picking your attribute in your entity 12 | and adding the following to the User Info: primaryKey true. 13 | Crack open the :code:`App.xcdatamodeld` and look at any of the entities 14 | for more info. 15 | 16 | 2. You need to be sure the `Class` you assign to the entity in your 17 | `xcdatamodeld` is a subclass of AmigoModel 18 | 19 | 20 | You do not have to use a :code:`NSManagedObjectModel` at all. In fact, 21 | we just use it to convert the :code:`NSEntityDescriptions` into 22 | :code: `ORMModel` instances. It's fine if you want to do the mappings 23 | yourself. See :doc:`orm_models` for more. 24 | -------------------------------------------------------------------------------- /AmigoTests/AmigoTestBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoTestBase.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/23/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import Amigo 12 | 13 | 14 | class AmigoTestBase: XCTestCase { 15 | 16 | var amigo: Amigo! 17 | var engine: SQLiteEngineFactory! 18 | 19 | override func setUp() { 20 | super.setUp() 21 | let name = "App" 22 | let bundle = NSBundle(forClass: self.dynamicType) 23 | let url = NSURL(string:bundle.pathForResource(name, ofType: "momd")!)! 24 | let mom = NSManagedObjectModel(contentsOfURL: url)! 25 | 26 | engine = SQLiteEngineFactory(":memory:", echo: true) 27 | amigo = Amigo(mom, factory: engine) 28 | amigo.createAll() 29 | } 30 | 31 | override func tearDown() { 32 | // Put teardown code here. This method is called after the invocation of each test method in the class. 33 | super.tearDown() 34 | (amigo.config.engine as! SQLiteEngine).db.close() 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /docs/setup/engine.rst: -------------------------------------------------------------------------------- 1 | Engine Setup 2 | ================================= 3 | 4 | An instance of Amigo needs an engine to run the queries. Amigo comes 5 | with a `SQLiteEngine` out of the box. Let's take a look at setting it 6 | up so we can create our schema: 7 | 8 | .. code-block:: swift 9 | 10 | import Amigo 11 | import CoreData 12 | 13 | // the first arg can be ":memory:" for an in-memory 14 | // database, or it can be the absolute path to your 15 | // sqlite database. 16 | // 17 | // echo : Boolean 18 | // true prints out the SQL statements with params 19 | // the default value of false does nothing. 20 | 21 | let mom = NSManagedObjectModel(contentsOfURL: url)! 22 | 23 | // specifying 'echo: true' will have amigo print out 24 | // all of the SQL commands it's generating. 25 | let engine = SQLiteEngineFactory(":memory:", echo: true) 26 | amigo = Amigo(mom, factory: engine) 27 | amigo.createAll() 28 | 29 | Note, in the example above `Amigo` can process a :code:`ManagedObjectModel` 30 | to create it's schema. See :doc:`models/mom` for more. 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 BLITZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Amigo/ThreadLocalEngineFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadLocalEngineFactory.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/22/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ThreadLocalEngineFactory: EngineFactory{ 12 | func createEngine() -> Engine 13 | } 14 | 15 | extension ThreadLocalEngineFactory{ 16 | 17 | func engineForCurrentThread() -> Engine? { 18 | let dict = NSThread.currentThread().threadDictionary 19 | let obj = dict["amigo.threadlocal.engine"] as? Engine 20 | 21 | if let obj = obj{ 22 | return obj 23 | } else { 24 | return nil 25 | } 26 | } 27 | 28 | func createEngineForCurrentThread() -> Engine { 29 | let engine = createEngine() 30 | let dict = NSThread.currentThread().threadDictionary 31 | dict["amigo.threadlocal.engine"] = engine as? AnyObject 32 | 33 | return engine 34 | } 35 | 36 | public func connect() -> Engine { 37 | if let engine = engineForCurrentThread(){ 38 | return engine 39 | } else { 40 | return createEngineForCurrentThread() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Amigo! 2 | ================================= 3 | 4 | A SQLite ORM for Swift 2.1+ powered by `FMDB `_ 5 | 6 | 7 | But What About CoreData? 8 | ------------------------ 9 | 10 | Yup, CoreData... 11 | 12 | 13 | Requirements 14 | ------------------------ 15 | 16 | - Swift 2.1 (aka iOS 9+, os x 10.11+) 17 | - CoreData 18 | 19 | 20 | The Deal 21 | ------------------------ 22 | 23 | So, we like CoreData, but we also like ORMs in the flavor of SQLAlchemy 24 | or Django's ORM. So lets make that happen, however unwisely, in Swift. 25 | To that end you could say at this point that Amigo was firstly inspired 26 | by the version of Amigo we wrote for C#/Xamarin which you can find here: 27 | 28 | https://github.com/blitzagency/amigo 29 | 30 | But what if we just want to work in Swift you say? Well, lets rewrite 31 | Amigo in Swift then! So we did. It's a start. A start that shamelessly 32 | copies, probably poorly, ideas found in SQLAlchemy and Django. 33 | 34 | 35 | 36 | Contents: 37 | 38 | .. toctree:: 39 | :maxdepth: 2 40 | 41 | setup/index 42 | models/index 43 | querying/index 44 | sessions/index 45 | 46 | 47 | Indices and tables 48 | ================== 49 | 50 | * :ref:`genindex` 51 | * :ref:`modindex` 52 | * :ref:`search` 53 | 54 | -------------------------------------------------------------------------------- /Amigo/SQLiteTypeCompiler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteTypeCompiler.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | public struct SQLiteTypeCompiler : TypeCompiler{ 14 | 15 | public func compileInteger16() -> String{ 16 | return "INTEGER" 17 | } 18 | 19 | public func compileInteger32() -> String{ 20 | return "INTEGER" 21 | } 22 | 23 | public func compileInteger64() -> String{ 24 | return "INTEGER" 25 | } 26 | 27 | public func compileString() -> String{ 28 | return "TEXT" 29 | } 30 | 31 | public func compileBoolean() -> String{ 32 | return "INTEGER" 33 | } 34 | 35 | public func compileDate() -> String{ 36 | return "TEXT" 37 | } 38 | 39 | public func compileBinaryData() -> String{ 40 | return "BLOB" 41 | } 42 | 43 | public func compileDecimal() -> String{ 44 | return "REAL" 45 | } 46 | 47 | public func compileDouble() -> String{ 48 | return "REAL" 49 | } 50 | 51 | public func compileFloat() -> String{ 52 | return "REAL" 53 | } 54 | 55 | public func compileUndefined() -> String{ 56 | return "BLOB" 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /AmigoTests/EntityDescriptionMapperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityDescriptionMapperTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/9/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import Amigo 12 | 13 | 14 | class EntityDescriptionMapperTests: XCTestCase { 15 | var mapper: EntityDescriptionMapper! 16 | var mom: NSManagedObjectModel! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | let name = "App" 22 | let bundle = NSBundle(forClass: self.dynamicType) 23 | let url = bundle.URLForResource(name, withExtension: "momd")! 24 | 25 | mom = NSManagedObjectModel(contentsOfURL: url)! 26 | mapper = EntityDescriptionMapper() 27 | } 28 | 29 | override func tearDown() { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | super.tearDown() 32 | } 33 | 34 | func testMapper() { 35 | let model = mapper.map(mom.entitiesByName["Dog"]!) 36 | XCTAssertEqual(model.table.label, "amigotests_dog") 37 | XCTAssertEqual(model.type, "AmigoTests.Dog") 38 | 39 | } 40 | 41 | func testOneToManyMapping(){ 42 | let entities = [ 43 | mom.entitiesByName["Author"]!, 44 | mom.entitiesByName["Post"]! 45 | ] 46 | 47 | let models = entities.map(mapper.map) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Amigo/Index.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Index.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Index: SchemaItem, CustomStringConvertible{ 12 | public let label: String 13 | public let unique: Bool 14 | 15 | public var columns: [Column]! 16 | public var table: Table? { 17 | didSet{ 18 | if let strings = columnStrings{ 19 | columns = strings.map{ table!.c[$0]! } 20 | } 21 | } 22 | } 23 | 24 | var columnStrings: [String]? 25 | 26 | public convenience init(_ label: String, unique: Bool = false, _ columns: String...){ 27 | self.init(label, unique: unique, columns: columns) 28 | } 29 | 30 | public convenience init(_ label: String, unique: Bool = false, columns: [String] ){ 31 | self.init(label, unique: unique, columns: nil) 32 | columnStrings = columns 33 | } 34 | 35 | public convenience init(_ label: String, unique: Bool = false, columns: Column...){ 36 | self.init(label, unique: unique, columns: columns) 37 | } 38 | 39 | public init(_ label: String, unique: Bool = false, columns maybeColumns: [Column]? ){ 40 | self.label = label 41 | self.unique = unique 42 | 43 | if let columns = maybeColumns{ 44 | self.columns = columns 45 | self.table = columns[0].table 46 | } 47 | } 48 | 49 | public var description: String{ 50 | return "" 51 | } 52 | } -------------------------------------------------------------------------------- /Amigo.xcodeproj/project.xcworkspace/xcshareddata/Amigo.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "76CDF08CE51248DC9FD59BC936CA918CEA0DC715", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "04101981B66E318109934C9516F88D824BB96459" : 0, 8 | "76CDF08CE51248DC9FD59BC936CA918CEA0DC715" : 0 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "2EAF0BE7-B796-4B0B-BD31-F6F210E71F42", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "04101981B66E318109934C9516F88D824BB96459" : "..\/..\/..\/..\/github\/fmdb", 13 | "76CDF08CE51248DC9FD59BC936CA918CEA0DC715" : "Amigo\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Amigo", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 203, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Amigo.xcodeproj", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:aventurella\/fmdb.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "04101981B66E318109934C9516F88D824BB96459" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:blitzagency\/amigo-swift.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "76CDF08CE51248DC9FD59BC936CA918CEA0DC715" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /AmigoTests/SelectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Amigo 11 | 12 | class SelectTests: XCTestCase { 13 | 14 | var meta: MetaData! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | meta = MetaData() 19 | 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | super.tearDown() 25 | } 26 | 27 | func testTableSelect() { 28 | let t1 = Table("dogs", metadata: meta, 29 | Column("id", type: Int.self, primaryKey: true), 30 | Column("label", type: String.self) 31 | ) 32 | 33 | let select = Select(t1) 34 | XCTAssertEqual(select.columns.count, 2) 35 | } 36 | 37 | func testTableSelectJoin(){ 38 | let t1 = Table("dogs", metadata: meta, 39 | Column("id", type: Int.self, primaryKey: true), 40 | Column("label", type: String.self) 41 | ) 42 | 43 | let t2 = Table("cats", metadata: meta, 44 | Column("id", type: Int.self, primaryKey: true), 45 | Column("label", type: String.self) 46 | ) 47 | 48 | let t3 = Table("people", metadata: meta, 49 | Column("id", type: Int.self, primaryKey: true), 50 | Column("label", type: String.self), 51 | Column("dog_id", type: ForeignKey(t1)), 52 | Column("cat_id", type: ForeignKey(t2)) 53 | ) 54 | 55 | let j1 = t3.join(t1) 56 | let j2 = t3.join(t2) 57 | let select = Select(t3).selectFrom(j1, j2) 58 | 59 | } 60 | 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Amigo/AmigoConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoConfiguration.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 6/29/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | public protocol AmigoConfigured{ 13 | var config: AmigoConfiguration {get} 14 | } 15 | 16 | extension AmigoConfigured{ 17 | var mapper: Mapper{ 18 | return config.mapper 19 | } 20 | 21 | var engine: Engine{ 22 | return config.engine 23 | } 24 | 25 | var tableIndex: [String: ORMModel]{ 26 | return config.tableIndex 27 | } 28 | 29 | var typeIndex: [String: ORMModel]{ 30 | return config.typeIndex 31 | } 32 | 33 | } 34 | 35 | public struct AmigoConfiguration{ 36 | 37 | public let engine: Engine 38 | public let mapper: Mapper 39 | public let tableIndex: [String: ORMModel] 40 | public let typeIndex: [String: ORMModel] 41 | 42 | 43 | static var documentsDirectory: String{ 44 | let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) 45 | return urls[urls.count-1].path! 46 | } 47 | 48 | // public static var defaultConfiguration: AmigoConfiguration{ 49 | // return AmigoConfiguration.fromName("App") 50 | // } 51 | 52 | // public static func fromName(name: String) -> AmigoConfiguration{ 53 | // let dir = AmigoConfiguration.documentsDirectory.stringByAppendingPathComponent("\(name).sqlite") 54 | // let engine = SQLiteEngine(path: dir) 55 | // let mapper = EntityDescriptionMapper() 56 | // 57 | // return AmigoConfiguration(engine: engine, mapper: mapper) 58 | // } 59 | // 60 | // public static func fromMemory() -> AmigoConfiguration{ 61 | // let engine = SQLiteEngine(path: ":memory:") 62 | // let mapper = EntityDescriptionMapper() 63 | // 64 | // return AmigoConfiguration(engine: engine, mapper: mapper) 65 | // } 66 | } -------------------------------------------------------------------------------- /Amigo/TypeCompiler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeCompiler.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | public protocol TypeCompiler{ 13 | func process(type: NSAttributeType) -> String 14 | func compileInteger16() -> String 15 | func compileInteger32() -> String 16 | func compileInteger64() -> String 17 | func compileString() -> String 18 | func compileBoolean() -> String 19 | func compileDate() -> String 20 | func compileBinaryData() -> String 21 | func compileDecimal() -> String 22 | func compileDouble() -> String 23 | func compileFloat() -> String 24 | func compileUndefined() -> String 25 | } 26 | 27 | 28 | extension TypeCompiler{ 29 | public func process(type: NSAttributeType) -> String { 30 | switch type{ 31 | case .Integer16AttributeType: 32 | return compileInteger16() 33 | case .Integer32AttributeType: 34 | return compileInteger32() 35 | case .Integer64AttributeType: 36 | return compileInteger64() 37 | case .StringAttributeType: 38 | return compileString() 39 | case .BooleanAttributeType: 40 | return compileBoolean() 41 | case .DateAttributeType: 42 | return compileDate() 43 | case .BinaryDataAttributeType: 44 | return compileBinaryData() 45 | case .DecimalAttributeType: 46 | return compileDecimal() 47 | case .DoubleAttributeType: 48 | return compileDouble() 49 | case .FloatAttributeType: 50 | return compileFloat() 51 | case .UndefinedAttributeType: 52 | return compileUndefined() 53 | default: 54 | // NSAttributeType.TransformableAttributeType 55 | // NSAttributeType.ObjectIDAttributeType 56 | return compileUndefined() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /AmigoTests/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 6/29/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | import Amigo 12 | 13 | // ---- To One ---- 14 | // A `People` can only have 1 dog and 1 Cat 15 | 16 | class Dog: AmigoModel{ 17 | dynamic var id: NSNumber! 18 | dynamic var label: String! 19 | } 20 | 21 | class Cat: AmigoModel{ 22 | dynamic var id: NSNumber! 23 | dynamic var label: String! 24 | } 25 | 26 | class People: AmigoModel{ 27 | dynamic var id: NSNumber! 28 | dynamic var label: String! 29 | dynamic var dog: Dog! 30 | dynamic var cat: Cat! 31 | } 32 | 33 | 34 | // ---- One To Many ---- 35 | // We only allow 1 `Author` per `Post` 36 | // But an author can have Many Posts 37 | 38 | class Author: AmigoModel { 39 | dynamic var id: NSNumber! 40 | dynamic var firstName: String! 41 | dynamic var lastName: String! 42 | } 43 | 44 | class Post: AmigoModel{ 45 | dynamic var id: NSNumber! 46 | dynamic var title: String! 47 | dynamic var author: Author! 48 | } 49 | 50 | 51 | // ---- Many To Many ---- 52 | // A Parent can have Many Children 53 | // and children can have Many Parents 54 | 55 | class Parent: AmigoModel{ 56 | dynamic var id: NSNumber! 57 | dynamic var label: String! 58 | } 59 | 60 | class Child: AmigoModel{ 61 | dynamic var id: NSNumber! 62 | dynamic var label: String! 63 | } 64 | 65 | // ---- Many To Many (through model) ---- 66 | 67 | class Workout: AmigoModel{ 68 | dynamic var id: NSNumber! 69 | dynamic var label: String! 70 | } 71 | 72 | class WorkoutExercise: AmigoModel{ 73 | dynamic var id: NSNumber! 74 | dynamic var label: String! 75 | } 76 | 77 | class WorkoutMeta: AmigoModel{ 78 | dynamic var id: NSNumber! 79 | dynamic var duration: NSNumber! 80 | dynamic var position: NSNumber! 81 | dynamic var exercise: WorkoutExercise! 82 | dynamic var workout: Workout! 83 | } -------------------------------------------------------------------------------- /AmigoTests/SQLiteFormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteFormatTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/14/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Amigo 11 | 12 | class SQLiteFormatTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | } 17 | 18 | override func tearDown() { 19 | super.tearDown() 20 | } 21 | 22 | func testEscapeNilWithQuotes() { 23 | let value: String? = nil 24 | let result = SQLiteFormat.escapeWithQuotes(value) 25 | XCTAssert(result == "NULL") 26 | } 27 | 28 | func testEscapeNilWithoutQuotes() { 29 | let value: String? = nil 30 | let result = SQLiteFormat.escapeWithoutQuotes(value) 31 | XCTAssert(result == "(NULL)") 32 | } 33 | 34 | func testEscapeWithQuotes() { 35 | let value = "test's" 36 | let result = SQLiteFormat.escapeWithQuotes(value) 37 | XCTAssert(result == "'test''s'") 38 | } 39 | 40 | func testEscapeWithoutQuotes() { 41 | let value = "test's" 42 | let result = SQLiteFormat.escapeWithoutQuotes(value) 43 | XCTAssert(result == "test''s") 44 | } 45 | 46 | func testEscapeBlob() { 47 | 48 | let uuid = NSUUID() 49 | var bytes = [UInt8](count: 16, repeatedValue: 0) 50 | uuid.getUUIDBytes(&bytes) 51 | 52 | let data = NSData(bytes: bytes, length: bytes.count) 53 | 54 | let hex = SQLiteFormat.hexStringWithData(data) 55 | let result = SQLiteFormat.escapeBlob(data) 56 | 57 | XCTAssert(result == "x'\(hex)'") 58 | } 59 | 60 | func testEscapeEmptyBlob() { 61 | 62 | let data = NSData() 63 | let result = SQLiteFormat.escapeBlob(data) 64 | 65 | XCTAssert(result == "NULL") 66 | } 67 | 68 | func testEscapeNilBlob() { 69 | 70 | var data: NSData? 71 | let result = SQLiteFormat.escapeBlob(data) 72 | 73 | XCTAssert(result == "NULL") 74 | } 75 | } -------------------------------------------------------------------------------- /AmigoTests/AmigoMeta.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoMeta.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/1/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | 12 | @testable import Amigo 13 | 14 | class AmigoMeta: XCTestCase { 15 | var amigo: Amigo! 16 | var config: AmigoConfiguration! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | let bundle = NSBundle(forClass: self.dynamicType) 22 | config = AmigoConfiguration.fromName("App", bundle: bundle) 23 | amigo = Amigo(config: config) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testCreateAll(){ 32 | //let meta = AmigoMetaData(amigo.managedObjectModel) 33 | //let engine = SQLiteEngine(path: NSURL(string:"")!) 34 | // engine.createAll(meta) 35 | } 36 | 37 | 38 | func testMetaModelForType() { 39 | //let meta = AmigoMetaData(amigo.managedObjectModel) 40 | //let value = meta.metaModelForType(Author) 41 | //XCTAssertNotNil(value) 42 | //let engine = SQLiteEngine(path: NSURL(string:"")!) 43 | //meta.createAll(engine) 44 | // This is an example of a functional test case. 45 | // Use XCTAssert and related functions to verify your tests produce the correct results. 46 | } 47 | 48 | func testSQLLiteEngine(){ 49 | // let session = amigo.session 50 | // let engine = session.config.engine as! SQLiteEngine 51 | // let query = session.query(Post) 52 | // 53 | // engine.createQueryFields(<#T##query: T##T#>, model: <#T##MetaModel#>) 54 | // .all() 55 | } 56 | 57 | 58 | 59 | func testPerformanceExample() { 60 | 61 | // This is an example of a performance test case. 62 | self.measureBlock() { 63 | // Put the code you want to measure the time of here. 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /AmigoTests/SQLiteEngineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/3/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import FMDB 11 | @testable import Amigo 12 | 13 | class SQLiteEngineTests: XCTestCase { 14 | 15 | var engine: SQLiteEngine! 16 | var meta: MetaData! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | meta = MetaData() 22 | engine = SQLiteEngine(":memory:") 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | super.tearDown() 28 | engine.db.close() 29 | } 30 | 31 | // func testCreateSchema(){ 32 | // let done = expectationWithDescription("Done") 33 | // 34 | // 35 | // let _ = Table("dogs", metadata: meta, 36 | // Column("id", type: Int.self, primaryKey: true), 37 | // Column("name", type: String.self) 38 | // ) 39 | // 40 | // meta.createAll(engine) 41 | // 42 | // engine.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"){ (result: FMResultSet) in 43 | // 44 | // //XCTAssert(result.count == 1) 45 | // //XCTAssert(result[0][0] as! String == "dogs") 46 | // done.fulfill() 47 | // } 48 | // 49 | // waitForExpectationsWithTimeout(5.0, handler:nil) 50 | // 51 | // } 52 | 53 | func testSelect(){ 54 | // let done = expectationWithDescription("Done") 55 | // 56 | // let _ = Table("dogs", metadata: meta, 57 | // Column("id", type: Int.self, primaryKey: true), 58 | // Column("name", type: String.self) 59 | // ) 60 | // 61 | // meta.createAll(engine) 62 | // 63 | // let master = Table("sqlite_master", metadata: meta, 64 | // Column("name", type: String.self) 65 | // ) 66 | // 67 | // engine.query(master.select()){ result in 68 | // XCTAssert(result.count == 1) 69 | // XCTAssert(result[0][0] as! String == "dogs") 70 | // done.fulfill() 71 | // } 72 | // 73 | // waitForExpectationsWithTimeout(5.0, handler:nil) 74 | 75 | } 76 | 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Amigo/SQLiteFormat.m: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteFormat.m 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/14/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "sqlite3.h" 11 | #import "SQLiteFormat.h" 12 | 13 | @implementation SQLiteFormat 14 | + (nonnull NSString *)format:(nullable char *)format, ...{ 15 | va_list ap; 16 | va_start(ap, format); 17 | 18 | char *result = sqlite3_vmprintf(format, ap); 19 | NSString * string = @(result); 20 | sqlite3_free(result); 21 | 22 | va_end(ap); 23 | return string; 24 | } 25 | 26 | + (nonnull NSString *)escapeWithQuotes:(nullable NSString *)value{ 27 | const char* utf8 = value.UTF8String; 28 | return [SQLiteFormat format:"%Q", utf8]; 29 | } 30 | 31 | + (nonnull NSString *)escapeWithoutQuotes:(nullable NSString *)value{ 32 | const char* utf8 = value.UTF8String; 33 | return [SQLiteFormat format:"%q", utf8]; 34 | } 35 | 36 | + (nonnull NSString *)escapeBlob:(nullable NSData *)value { 37 | if (value == nil){ 38 | return [self escapeWithQuotes: nil]; 39 | } 40 | 41 | NSString* hex = [self hexStringWithData:value]; 42 | 43 | if (hex == [NSString string]){ 44 | return [self escapeWithQuotes: nil]; 45 | } 46 | 47 | NSString* escapedHex = [self escapeWithoutQuotes: hex]; 48 | 49 | // http://permalink.gmane.org/gmane.comp.db.sqlite.general/64150 50 | // https://www.sqlite.org/lang_expr.html 51 | // BLOB literals are string literals containing hexadecimal data and 52 | // preceded by a single "x" or "X" character. Example: X'53514C697465' 53 | NSString* result = [NSString stringWithFormat:@"x'%@'", escapedHex]; 54 | 55 | return result; 56 | } 57 | 58 | + (nonnull NSString *)hexStringWithData:(nonnull NSData *)data{ 59 | // https://gist.github.com/hlung/6333269 60 | 61 | const unsigned char* dataBuffer = (const unsigned char *)[data bytes]; 62 | NSUInteger dataLength = [data length]; 63 | 64 | if (!dataBuffer) { 65 | return [NSString string]; 66 | } 67 | 68 | NSMutableString* hexString = [NSMutableString stringWithCapacity:(dataLength * 2)]; 69 | 70 | for (int i = 0; i < dataLength; ++i) { 71 | [hexString appendString:[NSString stringWithFormat: @"%02lx", (unsigned long)dataBuffer[i]]]; 72 | } 73 | 74 | return [NSString stringWithString: [hexString lowercaseString]]; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /Amigo/Table.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Table.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/3/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | public func ==(lhs: Table, rhs: Table) -> Bool { 13 | return lhs.hashValue == rhs.hashValue 14 | } 15 | 16 | 17 | public class Table: SchemaItem, FromClause{ 18 | public let label: String 19 | public let metadata: MetaData 20 | public var primaryKey: Column? 21 | public var columns = [String: Column]() 22 | public var indexes = [Index]() 23 | var model: ORMModel? 24 | 25 | var _sortedColumns = [Column]() // represents the order added 26 | 27 | public var hashValue: Int { 28 | return description.hashValue 29 | } 30 | 31 | public var sortedColumns: [Column]{ 32 | return _sortedColumns 33 | } 34 | 35 | 36 | public var c: [String: Column]{ 37 | return columns 38 | } 39 | 40 | public convenience init(_ label: String, metadata: MetaData, _ items: SchemaItem...){ 41 | self.init(label, metadata: metadata, items: items) 42 | } 43 | 44 | public init(_ label: String, metadata: MetaData, items: [SchemaItem]){ 45 | 46 | self.label = label 47 | self.metadata = metadata 48 | 49 | let addIndex = { (value: Index) -> () in 50 | value.table = self 51 | self.indexes.append(value) 52 | } 53 | 54 | let addColumn = { (value: Column) -> () in 55 | 56 | value._table = self 57 | self.columns[value.label.lowercaseString] = value 58 | self._sortedColumns.append(value) 59 | 60 | if value.primaryKey{ 61 | self.primaryKey = value 62 | } 63 | 64 | if value.indexed{ 65 | let label = "\(self.label)_\(value.label)_idx" 66 | if value.unique{ 67 | addIndex(Index(label, unique: true, columns:value)) 68 | } else { 69 | addIndex(Index(label, columns:value)) 70 | } 71 | } 72 | } 73 | 74 | items.filter{$0 is Column}.forEach{addColumn($0 as! Column)} 75 | items.filter{$0 is Index}.forEach{addIndex($0 as! Index)} 76 | 77 | metadata.tables[label] = self 78 | metadata._sortedTables.append(self) 79 | } 80 | 81 | public var description: String { 82 | return "" 83 | } 84 | } -------------------------------------------------------------------------------- /Amigo/SQLiteEngine+QuerySet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine+QuerySet.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/24/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | extension SQLiteEngine{ 13 | public func execute(queryset:QuerySet) -> [T]{ 14 | let select = queryset.compile() 15 | let sql = compiler.compile(select) 16 | let model = queryset.model 17 | let models = queryset.config.tableIndex 18 | let mapper = queryset.mapper 19 | var result = [T]() 20 | 21 | var objectMap = [ORMModel: String]() 22 | var params: [AnyObject]! 23 | 24 | if let predicateParams = select.predicateParams{ 25 | params = predicateParams 26 | } 27 | 28 | if select.from.count > 1{ 29 | model.foreignKeys.forEach{ (key, value)-> () in 30 | let ormModel = models[value.foreignKey!.relatedTable.label]! 31 | objectMap[ormModel] = key 32 | } 33 | } 34 | 35 | let modelMapper = { (results: FMResultSet) -> [T] in 36 | 37 | var rows = [T]() 38 | 39 | while results.next(){ 40 | var objs = [ORMModel: AmigoModel]() 41 | var currentModel = model 42 | 43 | objs[currentModel] = mapper.instanceFromString(model.type) 44 | 45 | select.columns.forEach{ 46 | var active: AmigoModel! 47 | currentModel = models[$0.table!.label]! 48 | 49 | if let obj = objs[currentModel]{ 50 | active = obj 51 | } else { 52 | let root = objs[model]! 53 | let key = objectMap[currentModel]! 54 | active = mapper.instanceFromString(currentModel.type) 55 | objs[currentModel] = active 56 | root.setValue(active, forKey: key) 57 | } 58 | 59 | let value = results.objectForColumnName($0.qualifiedLabel!) 60 | active.setValue($0.deserialize(value), forKey: $0.label) 61 | } 62 | 63 | rows.append(objs[model]! as! T) 64 | } 65 | 66 | results.close() 67 | return rows 68 | } 69 | 70 | result = execute(sql, params: params){ (results: FMResultSet) -> [T] in 71 | return modelMapper(results) 72 | } 73 | 74 | return result 75 | } 76 | } -------------------------------------------------------------------------------- /Amigo/SQLiteEngine+Master.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine+Master.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/22/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | public struct SQLiteMasterRow : CustomStringConvertible{ 13 | let type: String 14 | let name : String 15 | let tableName: String 16 | let rootpage: Int 17 | let sql: String 18 | 19 | public var description: String{ 20 | return "\(self.dynamicType)(\(tableName))" 21 | } 22 | } 23 | 24 | extension SQLiteEngine{ 25 | public func master() -> [SQLiteMasterRow]{ 26 | 27 | return execute("SELECT * FROM sqlite_master ORDER BY name"){ (result:FMResultSet) -> [SQLiteMasterRow] in 28 | var rows = [SQLiteMasterRow]() 29 | 30 | while result.next(){ 31 | let row = SQLiteMasterRow( 32 | type: result.stringForColumn("type"), 33 | name: result.stringForColumn("name"), 34 | tableName: result.stringForColumn("tbl_name"), 35 | rootpage: Int(result.intForColumn("rootpage")), 36 | sql: result.stringForColumn("sql") 37 | ) 38 | 39 | rows.append(row) 40 | } 41 | return rows 42 | } 43 | } 44 | 45 | public func masterTables() -> [SQLiteMasterRow]{ 46 | 47 | return execute("SELECT * FROM sqlite_master WHERE type='table' ORDER BY name"){ (result:FMResultSet) -> [SQLiteMasterRow] in 48 | var rows = [SQLiteMasterRow]() 49 | 50 | while result.next(){ 51 | let row = SQLiteMasterRow( 52 | type: result.stringForColumn("type"), 53 | name: result.stringForColumn("name"), 54 | tableName: result.stringForColumn("tbl_name"), 55 | rootpage: Int(result.intForColumn("rootpage")), 56 | sql: result.stringForColumn("sql") 57 | ) 58 | 59 | rows.append(row) 60 | } 61 | 62 | return rows 63 | } 64 | } 65 | 66 | public func masterTable(label: String) -> SQLiteMasterRow?{ 67 | 68 | return execute("SELECT * FROM sqlite_master WHERE type='table' AND name=? ORDER BY name", params: [label]){ (result:FMResultSet) -> SQLiteMasterRow? in 69 | var rows = [SQLiteMasterRow]() 70 | 71 | while result.next(){ 72 | let row = SQLiteMasterRow( 73 | type: result.stringForColumn("type"), 74 | name: result.stringForColumn("name"), 75 | tableName: result.stringForColumn("tbl_name"), 76 | rootpage: Int(result.intForColumn("rootpage")), 77 | sql: result.stringForColumn("sql") 78 | ) 79 | 80 | rows.append(row) 81 | } 82 | 83 | return rows.first 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Amigo/SQLiteEngine+Transactions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine+Transactions.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/19/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | import CoreData 12 | 13 | extension SQLiteEngine{ 14 | 15 | 16 | // public func add(obj: T, model: ORMModel){ 17 | // let insert = model.table.insert() 18 | // let sql = compiler.compile(insert) 19 | // var automaticPrimaryKey = false 20 | // 21 | // var values = [AnyObject]() 22 | // 23 | // for each in model.table.sortedColumns{ 24 | // if each.primaryKey && each.type == .Integer64AttributeType{ 25 | // automaticPrimaryKey = true 26 | // continue 27 | // } 28 | // 29 | // let value: AnyObject? 30 | // 31 | // 32 | // if let column = each.foreignKey{ 33 | // print("++++ WHEN SAVING FOREIGN KEYS +++ -> SQLiteEngine+Transactions -> add()") 34 | // continue 35 | // } else { 36 | // value = obj.valueForKey(each.label) 37 | // } 38 | // 39 | // values.append(value!) 40 | // } 41 | // 42 | // db.inDatabase { (db) -> Void in 43 | // db.executeUpdate(sql, withArgumentsInArray: values) 44 | // 45 | // if automaticPrimaryKey { 46 | // let result = db.executeQuery("SELECT last_insert_rowid();", withArgumentsInArray: nil) 47 | // result.next() 48 | // obj.setValue(result.objectForColumnIndex(0), forKey: model.primaryKey.label) 49 | // result.close() 50 | // } 51 | // } 52 | // } 53 | // 54 | // public func delete(obj: T, model: ORMModel){ 55 | // let id = model.primaryKey.label 56 | // let value = obj.valueForKey(id) 57 | // let delete = model.table.delete().filter("\(id) = \(value)") 58 | // //compiler.compile(<#T##expression: NSPredicate##NSPredicate#>, model: <#T##ORMModel#>, models: <#T##[String : ORMModel]#>) 59 | // //let sql = compiler.compile(delete) 60 | //// var automaticPrimaryKey = false 61 | //// 62 | //// var values = [AnyObject]() 63 | //// 64 | //// for each in model.table.sortedColumns{ 65 | //// if each.primaryKey && each.type == .Integer64AttributeType{ 66 | //// automaticPrimaryKey = true 67 | //// continue 68 | //// } 69 | //// 70 | //// let value: AnyObject? 71 | //// 72 | //// 73 | //// if let column = each.foreignKey{ 74 | //// continue 75 | //// } else { 76 | //// value = obj.valueForKey(each.label) 77 | //// } 78 | //// 79 | //// values.append(value!) 80 | //// } 81 | //// 82 | //// db.inDatabase { (db) -> Void in 83 | //// db.executeUpdate(sql, withArgumentsInArray: values) 84 | //// } 85 | // } 86 | 87 | } -------------------------------------------------------------------------------- /docs/sessions/index.rst: -------------------------------------------------------------------------------- 1 | Sessions 2 | ================================= 3 | 4 | 5 | When you go though :code:`amigo.session` using the provided 6 | :code:`SQLiteEngine` you automatically begin a SQL Transaction. 7 | 8 | If you would like your information to actually be persisted you must 9 | :code:`commit` the transaction. Once committed, the session will 10 | automatically begin a new transaciton for you. 11 | 12 | 13 | .. code-block:: swift 14 | 15 | import Amigo 16 | 17 | class Dog: AmigoModel{ 18 | dynamic var id: NSNumber! 19 | dynamic var label: String! 20 | } 21 | 22 | class Person: AmigoModel{ 23 | dynamic var id: NSNumber! 24 | dynamic var label: String! 25 | dynamic var dog: Dog! 26 | } 27 | 28 | let dog = ORMModel(Dog.self, 29 | Column("id", type: Int.self, primaryKey: true), 30 | Column("label", type: String.self), 31 | OneToMany("people", using: Person.self, on: "dog") 32 | ) 33 | 34 | let person = ORMModel(Person.self, 35 | Column("id", type: Int.self, primaryKey: true), 36 | Column("label", type: String.self), 37 | Column("dog", type: ForeignKey(dog)) 38 | ) 39 | 40 | // specifying 'echo: true' will have amigo print out 41 | // all of the SQL commands it's generating. 42 | let engine = SQLiteEngineFactory(":memory:", echo: true) 43 | amigo = Amigo([dog, person], factory: engine) 44 | amigo.createAll() 45 | 46 | let session = amigo.session 47 | let d1 = Dog() 48 | let d2 = Dog() 49 | 50 | d1.label = "Lucy" 51 | d2.label = "Ollie" 52 | 53 | session.add(d1, d2) 54 | session.commit() 55 | 56 | 57 | Upsert 58 | ------------------------ 59 | 60 | When inserting a model, you have the option to choose weather or not 61 | you would like this to be done as an insert or an upsert. In sqlite 62 | this translates to :code:`INSERT OR REPLACE`. To take advantage of this 63 | you need to pass an additional argument to :code:`session.add`. 64 | 65 | .. code-block:: swift 66 | 67 | session.add(myModel, upsert: true) 68 | 69 | 70 | Batching 71 | ------------------------ 72 | 73 | If you would like to batch a significant number of queries 74 | Amigo supports this for add and delete. 75 | 76 | .. code-block:: swift 77 | 78 | let session = amigo.session 79 | 80 | session.batch{ bacth in 81 | myAdds.forEach(batch.add) 82 | myDeletes.forEach(batch.add) 83 | myUpserts.forEach{ batch.add($0, upsert: true) } 84 | } 85 | 86 | This will take all of the generated sql and execute at once. It's a 87 | convenience over FMDB :code:`executeStatements` 88 | https://github.com/ccgus/fmdb#multiple-statements-and-batch-stuff 89 | 90 | .. important:: 91 | 92 | When you use this functionality Amigo does not make any 93 | updates to the source models. For example, doing an 94 | :code:`session.add` will modify the source model with 95 | the primary key assigned to it by immediately issuing a 96 | :code:`SELECT last_insert_rowid();`. However, :code:`batch.add` 97 | will not do this. 98 | 99 | .. warning:: 100 | 101 | If you use batching with Many-To-Many + Through Models you should 102 | have all of the information necessary in advance for the 103 | relationship. It's not required, but if you don't have all of the 104 | Foreign Keys and Primary Key for the record, Amigo will skip batching 105 | those items in favor of a regular :code:`session.add` to ensure 106 | it has the proper information. 107 | -------------------------------------------------------------------------------- /Amigo/NSManagedObjectModel+Sorted.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObject+Sorted.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/14/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | //extension NSEntityDescription{ 13 | // public override var description: String{ 14 | // return self.name! 15 | // } 16 | //} 17 | 18 | extension NSManagedObjectModel{ 19 | 20 | public func sortedEntities() -> [NSEntityDescription] { 21 | let list = buildDependencyList() 22 | return topologicalSort(list) 23 | } 24 | 25 | func validateTopologicalSort(graph: [NSEntityDescription:[NSEntityDescription]]) -> Bool { 26 | for (key, value) in graph { 27 | if value.count > 0 { 28 | fatalError("Dependency Cycle for Entity: \(key)") 29 | } 30 | } 31 | 32 | return true 33 | } 34 | 35 | public func topologicalSort(dependencyList:[NSEntityDescription:[NSEntityDescription]]) -> [NSEntityDescription]{ 36 | // https://gist.github.com/mrecachinas/0704e8110983fff94ae9 37 | 38 | var sorted = [NSEntityDescription]() 39 | var next_depth = [NSEntityDescription]() 40 | var graph = dependencyList 41 | 42 | for key in graph.keys { 43 | if graph[key]! == [] { 44 | next_depth.append(key) 45 | } 46 | } 47 | 48 | for key in next_depth { 49 | graph.removeValueForKey(key) 50 | } 51 | 52 | while next_depth.count != 0 { 53 | 54 | next_depth = next_depth.sort{ $0.name! > $1.name } 55 | 56 | let node = next_depth.removeLast() 57 | sorted.append(node) 58 | 59 | for key in graph.keys { 60 | let arr = graph[key] 61 | let dl = arr!.filter{ $0 == node} 62 | if dl.count > 0 { 63 | graph[key] = graph[key]?.filter({$0 != node}) 64 | if graph[key]?.count == 0 { 65 | next_depth.append(key) 66 | } 67 | } 68 | } 69 | } 70 | 71 | validateTopologicalSort(graph) 72 | return sorted 73 | } 74 | 75 | public func buildDependencyList() -> [NSEntityDescription:[NSEntityDescription]]{ 76 | var dependencies = [NSEntityDescription:[NSEntityDescription]]() 77 | 78 | // initialize the list 79 | for each in entities{ 80 | dependencies[each] = [] 81 | } 82 | 83 | for each in entities{ 84 | for (_, relationship) in each.relationshipsByName { 85 | 86 | if relationship.toMany == false{ 87 | dependencies[each]!.append(relationship.destinationEntity!) 88 | } 89 | } 90 | } 91 | 92 | return dependencies 93 | } 94 | 95 | public func buildDependencyListStrings() -> [String:[String]]{ 96 | var dependencies = [String:[String]]() 97 | 98 | // initialize the list 99 | for each in entities{ 100 | dependencies[each.name!] = [] 101 | } 102 | 103 | for each in entities{ 104 | for (_, relationship) in each.relationshipsByName { 105 | 106 | if relationship.toMany == false{ 107 | dependencies[each.name!]!.append(relationship.destinationEntity!.name!) 108 | } 109 | } 110 | } 111 | 112 | return dependencies 113 | } 114 | } -------------------------------------------------------------------------------- /AmigoTests/MOMExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MOMExtensionTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/14/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import Amigo 12 | 13 | class MOMExtensionTests: XCTestCase { 14 | 15 | var mom: NSManagedObjectModel! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | let name = "App" 20 | let bundle = NSBundle(forClass: self.dynamicType) 21 | let url = NSURL(string:bundle.pathForResource(name, ofType: "momd")!)! 22 | mom = NSManagedObjectModel(contentsOfURL: url)! 23 | 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testDependencyList() { 32 | let list = mom.buildDependencyList() 33 | 34 | // list.forEach{ key, item in 35 | // print("\(key.name): \(item.count)") 36 | // } 37 | 38 | 39 | let cat = mom.entitiesByName["Cat"]! 40 | let dog = mom.entitiesByName["Dog"]! 41 | let people = mom.entitiesByName["People"]! 42 | 43 | let post = mom.entitiesByName["Post"]! 44 | let author = mom.entitiesByName["Author"]! 45 | 46 | let parent = mom.entitiesByName["Parent"]! 47 | let child = mom.entitiesByName["Child"]! 48 | 49 | let workout = mom.entitiesByName["Workout"]! 50 | let workoutExercise = mom.entitiesByName["WorkoutExercise"]! 51 | let workoutMeta = mom.entitiesByName["WorkoutMeta"]! 52 | 53 | //[Post: [Author], Dog: [], Author: [], People: [Cat, Dog], Cat: [], Workout: [], WorkoutExercise: [], WorkoutMeta: [Workout, WorkoutExercise], Parent: [], Child: []] 54 | 55 | 56 | // Foreign Keys 57 | XCTAssert(list[author]!.count == 0) 58 | XCTAssert(list[post]!.count == 1) 59 | 60 | // Foreign Keys 61 | XCTAssert(list[people]!.count == 2) 62 | XCTAssert(list[dog]!.count == 0) 63 | XCTAssert(list[cat]!.count == 0) 64 | 65 | // Many to Many + Through Model 66 | XCTAssert(list[workout]!.count == 0) 67 | XCTAssert(list[workoutExercise]!.count == 0) 68 | XCTAssert(list[workoutMeta]!.count == 2) 69 | 70 | // Many To Many 71 | XCTAssert(list[parent]!.count == 0) 72 | XCTAssert(list[child]!.count == 0) 73 | } 74 | 75 | func testTopologicalSort() { 76 | 77 | let list = mom.buildDependencyList() 78 | let sorted = mom.topologicalSort(list) 79 | 80 | let author = mom.entitiesByName["Author"]! 81 | let post = mom.entitiesByName["Post"]! 82 | 83 | let cat = mom.entitiesByName["Cat"]! 84 | let dog = mom.entitiesByName["Dog"]! 85 | let people = mom.entitiesByName["People"]! 86 | 87 | let parent = mom.entitiesByName["Parent"]! 88 | let child = mom.entitiesByName["Child"]! 89 | 90 | let workout = mom.entitiesByName["Workout"]! 91 | let workoutExercise = mom.entitiesByName["WorkoutExercise"]! 92 | let workoutMeta = mom.entitiesByName["WorkoutMeta"]! 93 | 94 | // sorted.forEach{ 95 | // print($0.name) 96 | // } 97 | 98 | // [Author, Cat, Child, Dog, Parent, People, Post, Workout, WorkoutExercise, WorkoutMeta] 99 | let expected = [author, cat, child, dog, parent, people, post, workout, workoutExercise, workoutMeta] 100 | XCTAssert(sorted == expected) 101 | 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /AmigoTests/AmigoSessionAsyncTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoSessionAsyncTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/16/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Amigo 11 | 12 | 13 | class AmigoSessionAsyncTests: AmigoTestBase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | } 22 | 23 | func testAsyncActionNoResult() { 24 | let done = expectationWithDescription("done") 25 | let session = amigo.session 26 | 27 | let complete = { 28 | let results = session.query(Dog).all() 29 | XCTAssert(results.count == 1) 30 | 31 | done.fulfill() 32 | } 33 | 34 | session.async{ 35 | let d1 = Dog() 36 | d1.label = "lucy" 37 | session.add(d1) 38 | complete() 39 | } 40 | 41 | waitForExpectationsWithTimeout(1.0, handler: nil) 42 | 43 | } 44 | 45 | func testAsyncActionResult() { 46 | let done = expectationWithDescription("done") 47 | let session = amigo.session 48 | 49 | session.async{ () -> Dog in 50 | let d1 = Dog() 51 | d1.label = "lucy" 52 | session.add(d1) 53 | 54 | return d1 55 | 56 | }.then{ dog in 57 | 58 | let results = session.query(Dog).all() 59 | XCTAssert(results.count == 1) 60 | XCTAssert(results[0].id == dog.id) 61 | XCTAssert(results[0].label == dog.label) 62 | 63 | done.fulfill() 64 | } 65 | 66 | waitForExpectationsWithTimeout(1.0, handler: nil) 67 | 68 | } 69 | 70 | func testAsyncActionNoResultThen() { 71 | let done = expectationWithDescription("done") 72 | let session = amigo.session 73 | 74 | session.async{ 75 | let d1 = Dog() 76 | d1.label = "lucy" 77 | session.add(d1) 78 | 79 | }.then{ 80 | let results = session.query(Dog).all() 81 | XCTAssert(results.count == 1) 82 | 83 | done.fulfill() 84 | } 85 | 86 | waitForExpectationsWithTimeout(1.0, handler: nil) 87 | 88 | } 89 | 90 | func testAsyncActionCustomQueue() { 91 | let queue = dispatch_queue_create("com.amigo.async.tests", nil) 92 | 93 | let done = expectationWithDescription("done") 94 | let session = amigo.session 95 | 96 | session.async(queue: queue){ 97 | let d1 = Dog() 98 | d1.label = "lucy" 99 | session.add(d1) 100 | 101 | }.then{ 102 | let results = session.query(Dog).all() 103 | XCTAssert(results.count == 1) 104 | 105 | done.fulfill() 106 | } 107 | 108 | waitForExpectationsWithTimeout(60.0, handler: nil) 109 | 110 | } 111 | 112 | func testAsyncResultsArray() { 113 | 114 | let done = expectationWithDescription("done") 115 | let session = amigo.session 116 | 117 | let callback : ([Dog]) -> () = { results in 118 | XCTAssert(results.count == 1) 119 | done.fulfill() 120 | } 121 | 122 | session.async{ () -> [Dog] in 123 | let d1 = Dog() 124 | d1.label = "lucy" 125 | session.add(d1) 126 | return session.query(Dog).all() 127 | }.then(callback) 128 | 129 | 130 | waitForExpectationsWithTimeout(60.0, handler: nil) 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /AmigoTests/MetaTableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASTTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/5/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import Amigo 12 | 13 | class MetaTableTests: XCTestCase { 14 | 15 | var metadata: MetaData! 16 | 17 | override func setUp() { 18 | metadata = MetaData() 19 | super.setUp() 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | override func tearDown() { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | super.tearDown() 26 | } 27 | 28 | func testTableVariadicColumns() { 29 | 30 | let table = Table("dogs", metadata: metadata, 31 | Column("id", type: Int.self, primaryKey: true), 32 | Column("lucy", type: String.self) 33 | ) 34 | 35 | XCTAssertTrue(table.c.count == 2) 36 | } 37 | 38 | func testTableVariadicMixed() { 39 | 40 | let table = Table("dogs", metadata: metadata, 41 | Column("id", type: Int.self, primaryKey: true), 42 | Column("lucy", type: String.self), 43 | Index("dogs_lucy_idx", "lucy") 44 | ) 45 | 46 | XCTAssertTrue(table.c.count == 2) 47 | XCTAssertTrue(table.indexes.count == 1) 48 | } 49 | 50 | func testTableList() { 51 | 52 | let items:[SchemaItem] = [ 53 | Column("id", type: Int.self, primaryKey: true), 54 | Column("lucy", type: String.self)] 55 | 56 | let table = Table("dogs", metadata: metadata, items: items) 57 | 58 | XCTAssertTrue(table.c.count == 2) 59 | } 60 | 61 | func testMetadata() { 62 | 63 | let items: [SchemaItem] = [ 64 | Column("id", type: Int.self, primaryKey: true), 65 | Column("lucy", type: String.self)] 66 | 67 | let _ = Table("dogs", metadata: metadata, items: items) 68 | 69 | XCTAssertTrue(metadata.tables.values.count == 1) 70 | } 71 | 72 | func testColumnAliasAccess() { 73 | 74 | let items:[SchemaItem] = [ 75 | Column("id", type: Int.self, primaryKey: true), 76 | Column("lucy", type: String.self)] 77 | 78 | let table = Table("dogs", metadata: metadata, items: items) 79 | 80 | XCTAssertNotNil(table.c["id"]!) 81 | } 82 | 83 | func testColumnTableAssociation() { 84 | 85 | let items: [SchemaItem] = [ 86 | Column("id", type: Int.self, primaryKey: true), 87 | Column("lucy", type: String.self)] 88 | 89 | let table = Table("dogs", metadata: metadata, items: items) 90 | let id = table.c["id"]! 91 | 92 | if let _ = id.table { 93 | XCTAssertTrue(true) 94 | } 95 | else { 96 | XCTFail("Value isn't set") 97 | } 98 | } 99 | 100 | func testPrimaryKeyTrue() { 101 | 102 | let items: [SchemaItem] = [ 103 | Column("id", type: Int.self, primaryKey: true), 104 | Column("lucy", type: String.self)] 105 | 106 | let table = Table("dogs", metadata: metadata, items: items) 107 | 108 | XCTAssertNotNil(table.primaryKey) 109 | } 110 | 111 | func testPrimaryKeyFalse() { 112 | 113 | let items: [SchemaItem] = [ 114 | Column("id", type: Int.self), 115 | Column("lucy", type: String.self)] 116 | 117 | let table = Table("dogs", metadata: metadata, items: items) 118 | 119 | XCTAssertNil(table.primaryKey) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Amigo/Relationship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Relationship.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/24/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // there is probably a way to combine ForeignKey With relationship, they 12 | // both are a relationship afterall. Need to think about that abstraction. 13 | 14 | public protocol Relationship: MetaItem{ 15 | var label: String {get} 16 | var type: RelationshipType {get} 17 | 18 | } 19 | 20 | public enum RelationshipType{ 21 | case OneToMany, ManyToMany 22 | } 23 | 24 | public func ==(lhs: ManyToMany, rhs: ManyToMany) -> Bool { 25 | return lhs.hashValue == rhs.hashValue 26 | } 27 | 28 | public class ManyToMany: Relationship, CustomStringConvertible, Hashable{ 29 | public let label: String 30 | public let type = RelationshipType.ManyToMany 31 | 32 | var left: ORMModel! 33 | var right: ORMModel! 34 | var through: ORMModel? 35 | var tables: [String]! 36 | var throughModel: String? 37 | var associationTable: Table! 38 | var partial: ((type: String) -> Void)? 39 | 40 | public init(_ label: String, using: T.Type, throughModel: U.Type? = nil){ 41 | self.label = label 42 | self.partial = self.partialInit(using, throughModel: throughModel) 43 | } 44 | 45 | public init(_ label: String, tables:[String], throughModel: String? = nil){ 46 | self.label = label 47 | self.tables = tables.sort() 48 | 49 | if let throughModel = throughModel { 50 | self.throughModel = throughModel 51 | } 52 | } 53 | 54 | func partialInit 55 | (right: T.Type, throughModel: U.Type? = nil) 56 | (left: String){ 57 | 58 | let r = typeToTableName(right) 59 | let l = typeToTableName(left) 60 | 61 | self.tables = [l, r].sort() 62 | 63 | if let throughModel = throughModel{ 64 | self.throughModel = throughModel.description() 65 | } 66 | 67 | self.partial = nil 68 | } 69 | 70 | public lazy var tableName: String = { 71 | return self.tables.joinWithSeparator("_") 72 | }() 73 | 74 | public lazy var hashValue: Int = { 75 | return self.tables.joinWithSeparator("_").hashValue 76 | }() 77 | 78 | public var description: String{ 79 | return "" 80 | } 81 | } 82 | 83 | public class OneToMany: Relationship, CustomStringConvertible{ 84 | public let label: String 85 | public let type = RelationshipType.OneToMany 86 | 87 | let table: String 88 | var column: String! 89 | var originTable: String! 90 | 91 | public convenience init(_ label: String, using: T.Type){ 92 | let tableName = typeToTableName(using) 93 | self.init(label, table: tableName, column: nil) 94 | } 95 | 96 | public init(_ label: String, table: String, column: String? = nil){ 97 | self.label = label 98 | self.table = table 99 | self.column = column 100 | } 101 | 102 | func initOriginType(value: String){ 103 | originTable = typeToTableName(value) 104 | } 105 | 106 | public var description: String{ 107 | return "" 108 | } 109 | } 110 | 111 | 112 | //public class Relationship: MetaItem, CustomStringConvertible{ 113 | // public let label: String 114 | // public let type: RelationshipType 115 | // 116 | // let relatedTableLabel: String 117 | // let relatedColumnLabel: String 118 | // 119 | // public init(_ label: String, table: String, column: String, type: RelationshipType){ 120 | // self.label = label 121 | // self.type = type 122 | // self.relatedTableLabel = table 123 | // self.relatedColumnLabel = column 124 | // } 125 | // 126 | // public var description: String{ 127 | // return "" 128 | // } 129 | //} -------------------------------------------------------------------------------- /AmigoTests/PerfTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerfTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/28/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | @testable import Amigo 12 | 13 | class PerfTests: XCTestCase { 14 | 15 | var amigo: Amigo! 16 | var engine: SQLiteEngineFactory! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | let name = "App" 21 | let bundle = NSBundle(forClass: self.dynamicType) 22 | let url = NSURL(string:bundle.pathForResource(name, ofType: "momd")!)! 23 | let mom = NSManagedObjectModel(contentsOfURL: url)! 24 | 25 | engine = SQLiteEngineFactory(":memory:", echo: true) 26 | amigo = Amigo(mom, factory: engine) 27 | amigo.createAll() 28 | } 29 | 30 | override func tearDown() { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | super.tearDown() 33 | (amigo.config.engine as! SQLiteEngine).db.close() 34 | } 35 | 36 | func testExample() { 37 | // This is an example of a functional test case. 38 | // Use XCTAssert and related functions to verify your tests produce the correct results. 39 | } 40 | 41 | func testPerformanceBatch() { 42 | let session = amigo.session 43 | 44 | measureBlock{ 45 | for _ in 0..<10000{ 46 | let d = Dog() 47 | d.label = "foo" 48 | 49 | session.add(d) 50 | } 51 | 52 | session.commit() 53 | } 54 | } 55 | 56 | func testPerformancePerAdd() { 57 | 58 | measureBlock{ 59 | for _ in 0..<10000{ 60 | let session = self.amigo.session 61 | let d = Dog() 62 | d.label = "foo" 63 | 64 | session.add(d) 65 | session.commit() 66 | } 67 | } 68 | } 69 | 70 | func testBatchCreateItems() { 71 | var statements = [String]() 72 | let session = amigo.session 73 | 74 | let objs = (0..<10000).map{ _ -> Dog in 75 | let d = Dog() 76 | d.label = "lucy" 77 | return d 78 | } 79 | 80 | //AmigoSqlite3.test() 81 | 82 | self.measureBlock{ 83 | objs.forEach{ 84 | // let model = $0.amigoModel 85 | session.add($0) 86 | // let sql = session.insertSQL(model) 87 | // print(sql) 88 | // let params = session.insertParams($0) 89 | // //let sql = "something" 90 | // //statements.append(sql) 91 | } 92 | } 93 | 94 | } 95 | 96 | func testSQLiteBatchUpsert(){ 97 | let session = amigo.session 98 | 99 | let objs = (0..<10).map{ _ -> Dog in 100 | let d = Dog() 101 | d.label = "lucy's" 102 | return d 103 | } 104 | 105 | self.measureBlock{ 106 | session.batch{ batch in 107 | objs.forEach{ 108 | batch.add($0, upsert: false) 109 | } 110 | } 111 | } 112 | } 113 | 114 | func testBatchJoinItem() { 115 | var statements = [String]() 116 | 117 | for _ in 0..<20000{ 118 | let d = Dog() 119 | d.label = "foo" 120 | statements.append("INSERT INTO amigotests_dog (label) VALUES ('foo');") 121 | } 122 | 123 | self.measureBlock{ 124 | statements.joinWithSeparator("\n") 125 | } 126 | 127 | } 128 | 129 | func testBatchMerge() { 130 | let a1 = [1, 2] 131 | let a2 = [3, 4] 132 | var out = [Int]() 133 | 134 | out.appendContentsOf(a1) 135 | out.appendContentsOf(a2) 136 | 137 | print(out) 138 | 139 | 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amigo 2 | A SQLite ORM for Swift 2.1+ powered by FMDB 3 | 4 | Docs in progress are here: 5 | 6 | http://amigo.readthedocs.org/en/latest/ 7 | 8 | 9 | 10 | ## Carthage 11 | 12 | FMDB (https://github.com/ccgus/fmdb), 13 | as of v 2.6 FMDB, supports Carthage. 14 | 15 | Drop this into your `Cartfile`: 16 | 17 | ``` 18 | github "blitzagency/amigo-swift" ~> 0.3.0 19 | ``` 20 | 21 | 22 | ## Fire it up 23 | 24 | Lets create a schema: 25 | 26 | 27 | ```swift 28 | 29 | import Amigo 30 | import CoreData 31 | 32 | // the first arg can be ":memory:" for an in-memory 33 | // database, or it can be the absolute path to your 34 | // sqlite database. 35 | // 36 | // echo : Boolean 37 | // true prints out the SQL statements with params 38 | // the default value of false does nothing. 39 | 40 | let mom = NSManagedObjectModel(contentsOfURL: url)! 41 | 42 | // specifying 'echo: true' will have amigo print out 43 | // all of the SQL commands it's generating. 44 | let engine = SQLiteEngineFactory(":memory:", echo: true) 45 | amigo = Amigo(mom, factory: engine) 46 | amigo.createAll() 47 | ``` 48 | 49 | Yup, Amigo can turn NSEntityDescriptions along with their relationships 50 | into your tables for you. 51 | 52 | Read more about it here: 53 | 54 | http://amigo.readthedocs.org/en/latest/models/mom.html 55 | 56 | There are only a couple things to know. 57 | 58 | 1. Unlike CoreData, you need to specify your primary key field. This could 59 | totally be automated for you, we havent decided if we like that or not yet. 60 | You do this by picking your attribute in your entity and adding the following 61 | to the User Info: `primaryKey` `true`. Crack open the `App.xcdatamodeld` 62 | and look at any of the entities for more info. 63 | 64 | 2. You need to be sure the Class you assign to the entity in your `xcdatamodeld` 65 | is a subclass of `AmigoModel` 66 | 67 | You do not have to use a `ManagedObjectModel` either you can just define your 68 | mappings yourself as follows: 69 | 70 | Read more about it here: 71 | 72 | http://amigo.readthedocs.org/en/latest/models/orm_models.html 73 | 74 | ```swift 75 | import Amigo 76 | 77 | class Dog: AmigoModel{ 78 | dynamic var id: Int = 0 79 | dynamic var label: String! 80 | } 81 | 82 | let dog = ORMModel(Dog.self, 83 | Column("id", type: Int.self, primaryKey: true) 84 | Column("label", type: String.self) 85 | Index("dog_label_idx", "label") 86 | ) 87 | 88 | // you could achieve the same mapping this way: 89 | 90 | let dog = ORMModel(Dog.self, 91 | Column("id", type: Int.self, primaryKey: true) 92 | Column("label", type: String.self, indexed: true) 93 | ) 94 | 95 | // now initialize Amigo 96 | // specifying 'echo: true' will have amigo print out 97 | // all of the SQL commands it's generating. 98 | let engine = SQLiteEngineFactory(":memory:", echo: true) 99 | amigo = Amigo([dog], factory: engine) 100 | amigo.createAll() 101 | ``` 102 | 103 | Amigo Supports `ForeignKeys`, `OneToMany` and `ManyToMany` relationships. 104 | More on that later. 105 | 106 | ## Querying 107 | 108 | Using our mapping above, lets do some simple querying: 109 | 110 | You can also read more about this here: 111 | 112 | http://amigo.readthedocs.org/en/latest/querying/index.html 113 | 114 | 115 | ```swift 116 | import Amigo 117 | 118 | class Dog: AmigoModel{ 119 | dynamic var id: NSNumber! 120 | dynamic var label: String! 121 | } 122 | 123 | let dog = ORMModel(Dog.self, 124 | Column("id", type: Int.self, primaryKey: true) 125 | Column("label", type: String.self) 126 | ) 127 | 128 | // specifying 'echo: true' will have amigo print out 129 | // all of the SQL commands it's generating. 130 | let engine = SQLiteEngineFactory(":memory:", echo: true) 131 | amigo = Amigo([dog], factory: engine) 132 | amigo.createAll() 133 | 134 | // first lets add a dog 135 | 136 | let session = amigo.session 137 | let d1 = Dog() 138 | d1.label = "Lucy" 139 | 140 | session.add(d1) 141 | 142 | // now lets query: 143 | let results = session.query(Dog).all() 144 | print(results.count) 145 | ``` 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /Amigo/EntityDescriptionMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityDescriptionMapper.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/9/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | // need to make an intermediate model to query against. 12 | 13 | public class EntityDescriptionMapper: AmigoEntityDescriptionMapper{ 14 | 15 | public required init(){} 16 | 17 | public func map(entity: NSEntityDescription) -> ORMModel { 18 | let type = entity.managedObjectClassName 19 | var items: [MetaItem] 20 | 21 | items = Array(entity.propertiesByName.values).map(mapColumn) 22 | .filter{ $0 != nil } 23 | .map{ $0! } 24 | 25 | let model = ORMModel(type, properties: items) 26 | return model 27 | } 28 | 29 | func mapColumn(obj: NSPropertyDescription) -> MetaItem?{ 30 | var column: MetaItem? 31 | 32 | if obj is NSAttributeDescription{ 33 | column = mapAttributeColumn(obj as! NSAttributeDescription) 34 | } else if obj is NSRelationshipDescription{ 35 | column = mapRelationshipColumn(obj as! NSRelationshipDescription) 36 | } 37 | 38 | return column 39 | } 40 | 41 | func mapAttributeColumn(obj: NSAttributeDescription) -> MetaItem{ 42 | let name = obj.name 43 | let indexed = obj.indexed 44 | let optional = obj.optional 45 | let type = obj.attributeType 46 | let primaryKey: Bool 47 | 48 | 49 | if let pk = obj.userInfo?["primaryKey"] as? NSString{ 50 | primaryKey = pk.boolValue 51 | } else{ 52 | primaryKey = false 53 | } 54 | 55 | return Column(name, 56 | type: type, 57 | primaryKey: primaryKey, 58 | indexed: indexed, 59 | optional: optional) 60 | } 61 | 62 | func mapRelationshipColumn(obj: NSRelationshipDescription) -> MetaItem?{ 63 | 64 | if obj.toMany == false{ 65 | return mapToOneRelationship(obj) 66 | } else { 67 | return mapToManyRelationship(obj) 68 | } 69 | } 70 | 71 | func mapToManyRelationship(obj: NSRelationshipDescription) -> Relationship?{ 72 | guard let inverseRelationship = obj.inverseRelationship else { 73 | return nil 74 | } 75 | 76 | if inverseRelationship.toMany == true{ 77 | return mapManyToManyRelationship(obj) 78 | } else { 79 | return mapOneToManyRelationship(obj) 80 | } 81 | } 82 | 83 | func mapManyToManyRelationship(obj: NSRelationshipDescription) -> Relationship{ 84 | 85 | let label = obj.name 86 | let entity = obj.entity 87 | let relationship = obj.inverseRelationship! 88 | let relatedEntity = relationship.entity 89 | let tableName = dottedNameToTableName(entity.managedObjectClassName) 90 | let relatedTableName = dottedNameToTableName(relatedEntity.managedObjectClassName) 91 | let tables = [tableName, relatedTableName] 92 | let throughModel : String? 93 | 94 | if let through = obj.userInfo?["throughModel"]{ 95 | throughModel = through as? String 96 | } else{ 97 | throughModel = nil 98 | } 99 | 100 | return ManyToMany(label, tables: tables, throughModel: throughModel) 101 | } 102 | 103 | func mapOneToManyRelationship(obj: NSRelationshipDescription) -> Relationship{ 104 | let label = obj.name 105 | let relationship = obj.inverseRelationship! 106 | let entity = relationship.entity 107 | let tableName = dottedNameToTableName(entity.managedObjectClassName) 108 | 109 | return OneToMany(label, table: tableName, column: relationship.name) 110 | } 111 | 112 | func mapToOneRelationship(obj: NSRelationshipDescription) -> Column{ 113 | 114 | 115 | let name = obj.name 116 | let entity = obj.destinationEntity! 117 | let destinationTable = dottedNameToTableName(entity.managedObjectClassName) 118 | let table = ORMModel.metadata.tables[destinationTable]! 119 | 120 | return Column(name, type: ForeignKey(table)) 121 | } 122 | } -------------------------------------------------------------------------------- /Amigo/ORMModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ORMModel.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/12/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public func ==(lhs: ORMModel, rhs: ORMModel) -> Bool { 12 | return lhs.hashValue == rhs.hashValue 13 | } 14 | 15 | 16 | public class ORMModel: Hashable{ 17 | public static let metadata = MetaData() 18 | 19 | public let table: Table 20 | public let foreignKeys: [String:Column] 21 | public let relationships: [String:Relationship] 22 | public let columns: [Column] 23 | public let primaryKey: Column! 24 | public let type: String 25 | public let label: String 26 | public var throughModelRelationship: ManyToMany? 27 | public var sqlInsert: String? 28 | public var sqlUpsert: String? 29 | public var sqlUpdate: String? 30 | public var sqlDelete: String? 31 | public var sqlDeleteThrough = [String: String]() 32 | public var sqlInsertThrough = [String: String]() 33 | 34 | public convenience init(_ qualifiedType: T.Type, _ properties: MetaItem...){ 35 | let type = qualifiedType.description() 36 | self.init(type, properties: properties) 37 | } 38 | 39 | public convenience init(_ qualifiedType: String, properties: MetaItem...){ 40 | self.init(qualifiedType, properties: properties) 41 | } 42 | 43 | public init(_ qualifiedType: String, properties:[MetaItem]){ 44 | 45 | let schemaItems = properties.filter{$0 is SchemaItem}.map{ $0 as! SchemaItem} 46 | 47 | let relationshipList = properties.filter{$0 is Relationship}.map{ $0 as! Relationship } 48 | let nameParts = qualifiedType.unicodeScalars 49 | .split{ $0 == "." } 50 | .map{ String($0).lowercaseString } 51 | 52 | let tableName = nameParts.joinWithSeparator("_") 53 | var tmpForeignKeys = [String:Column]() 54 | var tmpColumns = [Column]() 55 | var tmpPrimaryKey: Column! 56 | var tmpRelationships = [String: Relationship]() 57 | 58 | 59 | relationshipList.forEach{ (each: Relationship) -> Void in 60 | if let m2m = each as? ManyToMany, let partial = m2m.partial{ 61 | partial(type: qualifiedType) 62 | } 63 | 64 | if let o2m = each as? OneToMany{ 65 | o2m.initOriginType(qualifiedType) 66 | } 67 | } 68 | 69 | type = qualifiedType 70 | label = nameParts[1] 71 | 72 | // as an ORMModel we do some fancy name mangling with foreignKey Columns: 73 | let transformedSchemaItems = schemaItems.map{ item -> SchemaItem in 74 | 75 | if let column = item as? Column, let fk = column.foreignKey{ 76 | let associatedPrimaryKeyName = fk.relatedColumn.label 77 | // behold! the fancy name mangling 78 | let label = "\(column.label)_\(associatedPrimaryKeyName)" 79 | 80 | let primaryKey = column.primaryKey 81 | let indexed = column.indexed 82 | let unique = column.unique 83 | let defaultValue = column.defaultValue 84 | 85 | let newColumn = Column(label, type: fk, primaryKey: primaryKey, indexed: indexed, unique: unique, defaultValue: defaultValue) 86 | tmpForeignKeys[column.label] = newColumn 87 | 88 | return newColumn as SchemaItem 89 | } 90 | 91 | return item 92 | } 93 | 94 | table = Table(tableName, metadata: ORMModel.metadata, items: transformedSchemaItems) 95 | relationshipList.forEach{tmpRelationships[$0.label] = $0} 96 | 97 | table.sortedColumns.forEach{ value -> () in 98 | 99 | guard value.foreignKey == nil else { 100 | return 101 | } 102 | 103 | tmpColumns.append(value) 104 | 105 | if value.primaryKey { 106 | tmpPrimaryKey = value 107 | } 108 | } 109 | 110 | foreignKeys = tmpForeignKeys 111 | columns = tmpColumns 112 | primaryKey = tmpPrimaryKey 113 | relationships = tmpRelationships 114 | 115 | AmigoModel.amigoModelIndex[qualifiedType] = self 116 | table.model = self 117 | } 118 | 119 | public var hashValue: Int{ 120 | return type.hashValue 121 | } 122 | } -------------------------------------------------------------------------------- /Amigo/AmigoSessionModelAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoSessionModelAction.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/16/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Used in Many-To-Many queries 13 | public class AmigoSessionModelAction{ 14 | let using: T 15 | let usingModel: ORMModel 16 | let session: AmigoSession 17 | var _relationship: String? 18 | 19 | public init(_ obj: T, model: ORMModel, session: AmigoSession){ 20 | self.using = obj 21 | self.usingModel = model 22 | self.session = session 23 | } 24 | 25 | public func relationship(value: String) -> AmigoSessionModelAction{ 26 | self._relationship = value 27 | return self 28 | } 29 | 30 | public func delete(other: U){ 31 | 32 | if let key = _relationship{ 33 | 34 | if let relationship = usingModel.relationships[key] as? ManyToMany{ 35 | 36 | if let throughModel = relationship.throughModel{ 37 | fatalError("Relationship is managed though: \(throughModel)") 38 | } 39 | 40 | let leftModel = session.config.tableIndex[relationship.tables[0]]! 41 | let rightModel = session.config.tableIndex[relationship.tables[1]]! 42 | let left: AmigoModel 43 | let right: AmigoModel 44 | 45 | if leftModel == usingModel{ 46 | left = using 47 | right = other 48 | } else { 49 | left = other 50 | right = using 51 | } 52 | 53 | let leftId = leftModel.primaryKey!.label 54 | let leftColumn = "\(leftModel.label)_\(leftId)" 55 | let leftParam = left.valueForKey(leftId)! 56 | 57 | let rightId = rightModel.primaryKey!.label 58 | let rightColumn = "\(rightModel.label)_\(rightId)" 59 | let rightParam = right.valueForKey(rightId)! 60 | 61 | var delete = relationship.associationTable.delete() 62 | 63 | let predicate = NSPredicate(format:" \(leftColumn) = \"\(leftParam)\" AND \(rightColumn) = \"\(rightParam)\"") 64 | 65 | let (filter, params) = session.engine.compiler.compile(predicate, 66 | table: relationship.associationTable, 67 | models: session.config.typeIndex) 68 | 69 | delete.filter(filter) 70 | 71 | let sql = session.engine.compiler.compile(delete) 72 | session.engine.execute(sql, params: params) 73 | } 74 | } 75 | } 76 | 77 | public func add(other: U...){ 78 | add(other) 79 | } 80 | 81 | public func add(other: [U]){ 82 | other.forEach(addModel) 83 | } 84 | 85 | public func addModel(other: U){ 86 | 87 | if let key = _relationship{ 88 | 89 | if let relationship = usingModel.relationships[key] as? ManyToMany{ 90 | 91 | if let throughModel = relationship.throughModel{ 92 | fatalError("Relationship is managed though: \(throughModel)") 93 | } 94 | 95 | let leftModel = session.config.tableIndex[relationship.tables[0]]! 96 | let rightModel = session.config.tableIndex[relationship.tables[1]]! 97 | let left: AmigoModel 98 | let right: AmigoModel 99 | 100 | if leftModel == usingModel{ 101 | left = using 102 | right = other 103 | } else { 104 | left = other 105 | right = using 106 | } 107 | 108 | let leftId = leftModel.primaryKey!.label 109 | let leftParam = left.valueForKey(leftId)! 110 | 111 | let rightId = rightModel.primaryKey!.label 112 | let rightParam = right.valueForKey(rightId)! 113 | 114 | let params = [leftParam, rightParam] 115 | let insert = relationship.associationTable.insert() 116 | let sql = session.engine.compiler.compile(insert) 117 | 118 | session.engine.execute(sql, params: params) 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Amigo/AmigoMetaData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoMeta.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/1/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | public class AmigoMetaData{ 14 | var tables = [String:MetaModel]() 15 | 16 | public init(_ managedObjectModel: NSManagedObjectModel){ 17 | self.initialize(managedObjectModel) 18 | } 19 | 20 | public func metaModelForType(type: T.Type) -> MetaModel{ 21 | let key = type.description() 22 | return metaModelForName(key) 23 | } 24 | 25 | public func metaModelForName(name: String) -> MetaModel{ 26 | return tables[name]! 27 | } 28 | 29 | func initialize(managedObjectModel: NSManagedObjectModel){ 30 | let lookup = managedObjectModel.entitiesByName 31 | // there may be a better way here, right now we ensure we have 32 | // all the models, then we need to go do some additional passes 33 | // to ensure the ForeignKey Relationships (ToOne) and the Many To Many 34 | // relationships (ToMany). 35 | // 36 | // the N*3 loops hurts my heart, but for now, it'a only done 37 | // on initialization. 38 | 39 | 40 | lookup.map{metaModelFromEntity($1)} 41 | zip(lookup.values, tables.values).map(initializeRelationships) 42 | 43 | // lookup.map{registerModel($1)} 44 | 45 | 46 | // zip(lookup.values, tables.values).map(initializeRelationships) 47 | // initializeRelationships($0, model: $1) 48 | // } 49 | 50 | // join.map{(x, y) -> String in 51 | // print(99) 52 | // return "" 53 | // }.count 54 | 55 | 56 | } 57 | 58 | func initializeRelationships(entity:NSEntityDescription, model: MetaModel){ 59 | 60 | var columns = [Column]() 61 | var foreignKeys = [ForeignKey]() 62 | 63 | // sqlite supports FK's with actions: 64 | // https://www.sqlite.org/foreignkeys.html#fk_actions 65 | // TODO determine if iOS 9+ sqlite supports this 66 | // adjust column creation accordingly 67 | 68 | entity.relationshipsByName 69 | .filter{ $1.toMany == false } 70 | .map{ _, relationship -> Void in 71 | 72 | let target = relationship.destinationEntity! 73 | let targetModel = metaModelForName(target.managedObjectClassName) 74 | 75 | let foreignKey = ForeignKey( 76 | label: relationship.name, 77 | type: targetModel.primaryKey.type, 78 | relatedType: targetModel.table.type, 79 | optional: relationship.optional) 80 | 81 | let column = Column( 82 | label: "\(relationship.name)_id", 83 | type: targetModel.primaryKey.type, 84 | primaryKey: false, 85 | indexed: true, 86 | optional: relationship.optional, 87 | unique: false, 88 | foreignKey: foreignKey) 89 | 90 | columns.append(column) 91 | foreignKeys.append(foreignKey) 92 | } 93 | 94 | let model = MetaModel( 95 | table: model.table, 96 | columns: model.columns + columns, 97 | primaryKey: model.primaryKey, 98 | foreignKeys: foreignKeys) 99 | 100 | tables[String(model.table.type)] = model 101 | 102 | } 103 | 104 | 105 | func registerModel(model: MetaModel){ 106 | 107 | } 108 | 109 | func metaModelFromEntity(entityDescription: NSEntityDescription){ 110 | let model: MetaModel 111 | var primaryKey: Column? 112 | let table = Table.fromEntityDescription(entityDescription) 113 | var columns = entityDescription.attributesByName.map { 114 | Column.fromAttributeDescription($1) 115 | } 116 | 117 | primaryKey = columns.filter{ $0.primaryKey }.first 118 | if primaryKey == nil{ 119 | primaryKey = Column.defaultPrimaryKey() 120 | columns = [primaryKey!] + columns 121 | } 122 | 123 | model = MetaModel(table: table, columns: columns, primaryKey: primaryKey!) 124 | tables[String(table.type)] = model 125 | } 126 | 127 | func createAll(engine: Engine){ 128 | 129 | } 130 | } -------------------------------------------------------------------------------- /AmigoTests/MetaColumnTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaColumnTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/6/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import Amigo 12 | 13 | class MetaColumnTests: XCTestCase { 14 | 15 | 16 | func testString() { 17 | let column = Column("test", type: String.self) 18 | XCTAssertTrue(column.type == NSAttributeType.StringAttributeType) 19 | } 20 | 21 | func testInt16() { 22 | let column = Column("test", type: Int16.self) 23 | XCTAssertTrue(column.type == NSAttributeType.Integer16AttributeType) 24 | } 25 | 26 | func testInt32() { 27 | let column = Column("test", type: Int32.self) 28 | XCTAssertTrue(column.type == NSAttributeType.Integer32AttributeType) 29 | } 30 | 31 | func testInt64() { 32 | let column = Column("test", type: Int64.self) 33 | XCTAssertTrue(column.type == NSAttributeType.Integer64AttributeType) 34 | } 35 | 36 | func testInt() { 37 | let column = Column("test", type: Int.self) 38 | XCTAssertTrue(column.type == NSAttributeType.Integer64AttributeType) 39 | } 40 | 41 | func testBool() { 42 | let column = Column("test", type: Bool.self) 43 | XCTAssertTrue(column.type == NSAttributeType.BooleanAttributeType) 44 | } 45 | 46 | func testDouble() { 47 | let column = Column("test", type: Double.self) 48 | XCTAssertTrue(column.type == NSAttributeType.DoubleAttributeType) 49 | } 50 | 51 | func testFloat() { 52 | let column = Column("test", type: Float.self) 53 | XCTAssertTrue(column.type == NSAttributeType.FloatAttributeType) 54 | } 55 | 56 | func testUInt8Buffer() { 57 | let column = Column("test", type: [UInt8].self) 58 | XCTAssertTrue(column.type == NSAttributeType.BinaryDataAttributeType) 59 | } 60 | 61 | func testNSData() { 62 | let column = Column("test", type: NSData.self) 63 | XCTAssertTrue(column.type == NSAttributeType.BinaryDataAttributeType) 64 | } 65 | 66 | func testNSDate() { 67 | let column = Column("test", type: NSDate.self) 68 | XCTAssertTrue(column.type == NSAttributeType.DateAttributeType) 69 | } 70 | 71 | func testNSString() { 72 | let column = Column("test", type: NSString.self) 73 | XCTAssertTrue(column.type == NSAttributeType.StringAttributeType) 74 | } 75 | 76 | func testDecimalNumber() { 77 | let column = Column("test", type: NSDecimalNumber.self) 78 | XCTAssertTrue(column.type == NSAttributeType.DecimalAttributeType) 79 | } 80 | 81 | func testNoDefaultValueWithNil() { 82 | let column = Column("test", type: String.self) 83 | 84 | let value = column.valueOrDefault(nil) 85 | XCTAssert(value == nil) 86 | } 87 | 88 | func testNoDefaultValueWithValue() { 89 | let column = Column("test", type: String.self) 90 | 91 | let value = column.valueOrDefault("ollie") as! String 92 | XCTAssert(value == "ollie") 93 | } 94 | 95 | func testDefaultValue() { 96 | let column = Column("test", type: String.self){ 97 | return "ollie" 98 | } 99 | 100 | let value = column.valueOrDefault(nil) as! String 101 | XCTAssert(value == "ollie") 102 | } 103 | 104 | func testDefaultValueSkip() { 105 | let column = Column("test", type: String.self){ 106 | return "ollie" 107 | } 108 | 109 | let value = column.valueOrDefault("lucy") as! String 110 | XCTAssert(value == "lucy") 111 | } 112 | 113 | func testForeignKeyColumn() { 114 | let meta = MetaData() 115 | let t1 = Table("dogs", metadata: meta, 116 | Column("id", type: Int.self, primaryKey: true), 117 | Column("label", type: String.self) 118 | ) 119 | 120 | XCTAssert(t1.c["id"]!.table != nil) 121 | 122 | let t2 = Table("owner", metadata: meta, 123 | Column("id", type: Int.self, primaryKey: true), 124 | Column("label", type: String.self), 125 | Column("dog_id", type: ForeignKey(t1.c["id"]!)) 126 | ) 127 | 128 | XCTAssertTrue(t2.c["dog_id"]!.type == t1.primaryKey!.type) 129 | } 130 | 131 | func testForeignKeyTable() { 132 | let meta = MetaData() 133 | let t1 = Table("dogs", metadata: meta, 134 | Column("id", type: Int.self, primaryKey: true), 135 | Column("label", type: String.self) 136 | ) 137 | 138 | XCTAssert(t1.c["id"]!.table != nil) 139 | 140 | let t2 = Table("owner", metadata: meta, 141 | Column("id", type: Int.self, primaryKey: true), 142 | Column("label", type: String.self), 143 | Column("dog_id", type: ForeignKey(t1)) 144 | ) 145 | 146 | XCTAssertTrue(t2.c["dog_id"]!.type == t1.primaryKey!.type) 147 | } 148 | 149 | 150 | } 151 | -------------------------------------------------------------------------------- /Amigo/Column.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Column.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/3/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | public func ==(lhs: Column, rhs: Column) -> Bool { 13 | return lhs.hashValue == rhs.hashValue 14 | } 15 | 16 | 17 | public class Column: SchemaItem, CustomStringConvertible, Hashable{ 18 | 19 | public let label: String 20 | public let type: NSAttributeType 21 | public let primaryKey: Bool 22 | public let indexed: Bool 23 | public let unique: Bool 24 | public var optional: Bool 25 | public let defaultValue: (() -> AnyObject?)? 26 | 27 | public var hashValue: Int{ 28 | return description.hashValue 29 | } 30 | 31 | public func modelValue(model: AmigoModel) -> AnyObject? { 32 | return model.valueForKey(label) 33 | } 34 | 35 | public func valueOrDefault(model: AmigoModel) -> AnyObject? { 36 | return valueOrDefault(modelValue(model)) 37 | } 38 | 39 | public func valueOrDefault(value: AnyObject?) -> AnyObject? { 40 | if let defaultValue = defaultValue where value == nil{ 41 | return defaultValue() 42 | } 43 | 44 | return value 45 | } 46 | 47 | public func serialize(value: AnyObject?) -> AnyObject? { 48 | return value 49 | } 50 | 51 | public func deserialize(value: AnyObject?) -> AnyObject? { 52 | return value 53 | } 54 | 55 | var _foreignKey: ForeignKey? 56 | public var foreignKey: ForeignKey? { 57 | get{ 58 | return _foreignKey 59 | } 60 | } 61 | 62 | var _table: Table? { 63 | didSet{ 64 | _qualifiedLabel = "\(_table!.label).\(label)" 65 | } 66 | } 67 | 68 | public var table: Table? { 69 | return _table 70 | } 71 | 72 | var _qualifiedLabel: String? 73 | public var qualifiedLabel: String? { 74 | return _qualifiedLabel 75 | } 76 | 77 | public init(_ label: String, type: NSAttributeType, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil){ 78 | self.label = label 79 | self.type = type 80 | self.primaryKey = primaryKey 81 | self.indexed = indexed 82 | self.optional = optional 83 | self.unique = unique 84 | self.defaultValue = defaultValue 85 | } 86 | 87 | public convenience init(_ label: String, type: Any.Type, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil){ 88 | let attrType: NSAttributeType 89 | 90 | switch type{ 91 | case is NSString.Type: 92 | attrType = .StringAttributeType 93 | case is String.Type: 94 | attrType = .StringAttributeType 95 | case is Int16.Type: 96 | attrType = .Integer16AttributeType 97 | case is Int32.Type: 98 | attrType = .Integer32AttributeType 99 | case is Int64.Type: 100 | attrType = .Integer64AttributeType 101 | case is Int.Type: 102 | attrType = .Integer64AttributeType 103 | case is NSDate.Type: 104 | attrType = .DateAttributeType 105 | case is [UInt8].Type: 106 | attrType = .BinaryDataAttributeType 107 | case is NSData.Type: 108 | attrType = .BinaryDataAttributeType 109 | case is NSDecimalNumber.Type: 110 | attrType = .DecimalAttributeType 111 | case is Double.Type: 112 | attrType = .DoubleAttributeType 113 | case is Float.Type: 114 | attrType = .FloatAttributeType 115 | case is Bool.Type: 116 | attrType = .BooleanAttributeType 117 | default: 118 | attrType = .UndefinedAttributeType 119 | } 120 | 121 | self.init(label, type: attrType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 122 | 123 | } 124 | 125 | public convenience init(_ label: String, type: ForeignKey, primaryKey: Bool = false, indexed: Bool = true, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil){ 126 | let associatedType = type.relatedColumn.type 127 | self.init(label, type: associatedType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 128 | _foreignKey = ForeignKey(type.relatedColumn, column: self) 129 | } 130 | 131 | 132 | public var description: String { 133 | if let t = table{ 134 | return ":\(label), primaryKey:\(primaryKey), indexed: \(indexed), optional: \(optional), unique:\(unique)>" 135 | } else { 136 | return "" 137 | } 138 | 139 | } 140 | } -------------------------------------------------------------------------------- /Amigo/SQLiteEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQLiteEngine.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/2/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | 13 | 14 | public class SQLiteEngine: NSObject, Engine{ 15 | public let fetchLastRowIdAfterInsert = true 16 | public let _compiler = SQLiteCompiler() 17 | public var db: FMDatabaseQueue 18 | public let path: String? 19 | 20 | let echo: Bool 21 | var savepoints = [String]() 22 | 23 | public var compiler: Compiler{ 24 | return _compiler 25 | } 26 | 27 | public init(_ path: String?, echo: Bool = false){ 28 | self.path = path 29 | self.echo = echo 30 | 31 | if path == ":memory:" || path == nil { 32 | db = FMDatabaseQueue(path: nil) 33 | } else{ 34 | db = FMDatabaseQueue(path: path) 35 | } 36 | } 37 | 38 | public func createBatchOperation(session: AmigoSession) -> AmigoBatchOperation{ 39 | return SQLiteBatchOperation(session: session) 40 | } 41 | 42 | 43 | public func beginTransaction(){ 44 | var inTransaction: Bool = false 45 | db.inDatabase{ inTransaction = $0.inTransaction() } 46 | 47 | if inTransaction { 48 | let savepoint = "amigo.sqllite.\(savepoints.count)" 49 | savepoints.append(savepoint) 50 | 51 | if self.echo{ 52 | self.echo("SAVEPOINT", params: [savepoint]) 53 | } 54 | 55 | db.inDatabase{ 56 | do{ 57 | try $0.startSavePointWithName(savepoint) 58 | } catch _ { 59 | debugPrint("failed to start savepoint: \(savepoint)") 60 | } 61 | } 62 | } else { 63 | if self.echo{ 64 | self.echo("BEGIN TRANSACTION", params: nil) 65 | } 66 | 67 | db.inDatabase{$0.beginTransaction()} 68 | } 69 | } 70 | 71 | public func commitTransaction(){ 72 | 73 | if savepoints.count > 0{ 74 | let savepoint = savepoints.removeLast() 75 | db.inDatabase{ 76 | do { 77 | try $0.releaseSavePointWithName(savepoint) 78 | } catch _ { 79 | debugPrint("unable to release savepoint: \(savepoint)") 80 | } 81 | } 82 | 83 | } else { 84 | 85 | if self.echo{ 86 | self.echo("COMMIT TRANSACTION", params: nil) 87 | } 88 | db.inDatabase{$0.commit()} 89 | } 90 | } 91 | 92 | public func rollback(){ 93 | if savepoints.count > 0{ 94 | let savepoint = savepoints.removeLast() 95 | 96 | if self.echo{ 97 | self.echo("ROLLBACK TO SAVEPOINT", params: [savepoint]) 98 | } 99 | 100 | db.inDatabase{ 101 | do { 102 | try $0.rollbackToSavePointWithName(savepoint) 103 | } catch _ { 104 | debugPrint("unable to rollback to savepoint: \(savepoint)") 105 | } 106 | 107 | } 108 | } else { 109 | 110 | if self.echo{ 111 | self.echo("ROLLBACK TRANSACTION", params: nil) 112 | } 113 | db.inDatabase{$0.rollback()} 114 | } 115 | } 116 | 117 | public func lastrowid() -> Int{ 118 | 119 | let result = execute("SELECT last_insert_rowid();"){ (results: FMResultSet) -> Int in 120 | results.next() 121 | 122 | let index = Int(results.intForColumnIndex(0)) 123 | results.close() 124 | return index 125 | } 126 | 127 | return result 128 | } 129 | 130 | func echo(sql: String, params:[AnyObject]! = nil){ 131 | print("------------------") 132 | print("[PARAMS] \(params)") 133 | print("[SQL] \(sql)") 134 | print("------------------") 135 | } 136 | 137 | public func execute(sql: String, params: [AnyObject]! = nil, mapper: Input -> Output) -> Output{ 138 | var output: Output? 139 | 140 | db.inDatabase{ db in 141 | 142 | if self.echo{ 143 | self.echo(sql, params: params) 144 | } 145 | 146 | let input = db.executeQuery(sql, withArgumentsInArray: params) as! Input 147 | output = mapper(input) 148 | } 149 | 150 | return output! 151 | } 152 | 153 | 154 | public func execute(sql: String){ 155 | db.inDatabase{ db in 156 | 157 | if self.echo{ 158 | self.echo(sql) 159 | } 160 | 161 | db.executeStatements(sql) 162 | } 163 | } 164 | 165 | public func execute(sql: String, params: [AnyObject]! = nil){ 166 | 167 | db.inDatabase{ db in 168 | 169 | if self.echo{ 170 | self.echo(sql, params: params) 171 | } 172 | 173 | db.executeUpdate(sql, withArgumentsInArray: params) 174 | } 175 | 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /docs/setup/index.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ================================= 3 | 4 | 5 | Carthage 6 | ---------------------------------- 7 | 8 | FMDB (https://github.com/ccgus/fmdb), 9 | as of v 2.6 FMDB, does suport Carthage. 10 | 11 | Drop this into your :code:`Cartfile`: 12 | 13 | :: 14 | 15 | github "blitzagency/amigo-swift" ~> 0.3.1 16 | 17 | Admittedly, we are probably not the best at the whole, 18 | *"How do you share an Xcode Project"* thing, so any recommendations 19 | to imporve this process are welcome. 20 | 21 | Initialization Using A Closure 22 | ---------------------------------- 23 | 24 | Initialize Amigo into a global variable so all the initialization 25 | is done in one place: 26 | 27 | .. code-block:: swift 28 | 29 | import Amigo 30 | 31 | 32 | class Dog: AmigoModel{ 33 | dynamic var id = 0 34 | dynamic var label = "" 35 | } 36 | 37 | let amigo: Amigo = { 38 | let dog = ORMModel(Dog.self, 39 | IntegerField("id", primaryKey: true), 40 | CharField("label") 41 | ) 42 | 43 | // now initialize Amigo 44 | // specifying 'echo: true' will have amigo print out 45 | // all of the SQL commands it's generating. 46 | let engine = SQLiteEngineFactory(":memory:", echo: true) 47 | let amigo = Amigo([dog], factory: engine) 48 | amigo.createAll() 49 | 50 | return amigo 51 | }() 52 | 53 | 54 | .. note:: 55 | 56 | This creates the :code:`amigo` object lazily, which means it's not 57 | created until it's actually needed. This delays the initial 58 | output of the app information details. Because of this, 59 | we recommend forcing the :code:`amigo` object to be created 60 | at app launch by just referencing :code:`amigo` at the top of 61 | your :code:`didFinishLaunching` method if you don't 62 | already use the :code:`amigo` object for something on app launch. 63 | This style and description was taken directly from [XCGLogger]_ 64 | 65 | 66 | A Note About Threads and Async 67 | ------------------------------- 68 | 69 | Out of the box, Amigo uses FMDB's :code:`FMDatabaseQueue` to perform all 70 | of it work. This should set you up to user Amigo from any thread you like. 71 | 72 | Keep in mind though, the dispatch queue that :code:`FMDatabasesQueue` uses 73 | is a serial queue. So if you invoke multiple long running amigo commands 74 | from separate threads you will be waiting your turn in the serial queue 75 | before your command is executed. It's probably best to: 76 | 77 | .. code-block:: swift 78 | 79 | func doStuff(callback: ([YourModelType]) -> ()){ 80 | let session = amigo.session 81 | // any background queue you like 82 | let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 83 | 84 | disptatch_async(queue) { 85 | let results = session.query(YourModelType).all() // whatever amigo command you want 86 | dispatch_async(dispatch_get_main_queue()){ 87 | callback(results) 88 | } 89 | } 90 | } 91 | 92 | If that's too verbose for your liking you are also welcome to use the 93 | :code:`async` convenience methods provided by the amigo session. The 94 | above code would then look like this: 95 | 96 | .. code-block:: swift 97 | 98 | func doStuff(callback: ([YourModelType]) -> ()){ 99 | let session = amigo.session 100 | session.async{ () -> [YourModelType] in 101 | let results = session.query(YourModelType).all() 102 | return results 103 | }.then(callback) 104 | } 105 | 106 | There are a few ways to use these async handlers. The variations revolve 107 | around weather or not you are returning results. Check out the unit tests 108 | [AmigoSessionAsyncTests]_ for more examples. 109 | 110 | For example, you don't have to return any result at all: 111 | 112 | .. code-block:: swift 113 | 114 | func addObject(){ 115 | let session = amigo.session 116 | session.async{ 117 | let dog = Dog() 118 | dog.label = "Lucy" 119 | session.add(dog) 120 | } 121 | } 122 | 123 | func addBatch(){ 124 | let session = amigo.session 125 | session.async{ 126 | 127 | let d1 = Dog() 128 | d1.label = "Lucy" 129 | 130 | let d2 = Dog() 131 | d2.label = "Ollie" 132 | 133 | session.batch{ batch in 134 | batch.add([d1, d2]) 135 | } 136 | } 137 | } 138 | 139 | You can also specify your own background queue to execute on: 140 | 141 | .. code-block:: swift 142 | 143 | let queue = dispatch_queue_create("com.amigo.async.tests", nil) 144 | 145 | func addDoStuffOnMyOwnQueue(){ 146 | let session = amigo.session 147 | session.async(queue: queue){ 148 | let dog = Dog() 149 | dog.label = "Lucy" 150 | session.add(dog) 151 | } 152 | } 153 | 154 | 155 | Contents: 156 | 157 | .. toctree:: 158 | :maxdepth: 2 159 | 160 | engine 161 | 162 | 163 | .. [XCGLogger] XCGLogger Closure Initialization 164 | https://github.com/DaveWoodCom/XCGLogger#initialization-using-a-closure 165 | 166 | .. [AmigoSessionAsyncTests] AmigoSessionAsyncTests 167 | https://github.com/blitzagency/amigo-swift/blob/master/AmigoTests/AmigoSessionAsyncTests.swift 168 | -------------------------------------------------------------------------------- /Amigo/Fields.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fields.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 1/3/16. 6 | // Copyright © 2016 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class UUIDField: Column{ 12 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 13 | self.init(label, type: .BinaryDataAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 14 | } 15 | 16 | public override func serialize(value: AnyObject?) -> AnyObject?{ 17 | 18 | guard let string = value as? String, 19 | let uuid = NSUUID(UUIDString: string) else { 20 | return nil 21 | } 22 | 23 | var bytes = [UInt8](count: 16, repeatedValue: 0) 24 | uuid.getUUIDBytes(&bytes) 25 | 26 | return NSData(bytes: bytes, length: bytes.count) 27 | } 28 | 29 | /// Deserializes `UUID` Bytes back to their `String` representation 30 | /// 31 | /// - Attention: 32 | /// 33 | /// Each field is treated as an integer and has its value printed as a 34 | /// zero-filled hexadecimal digit string with the most significant 35 | /// digit first. The hexadecimal values "a" through "f" are output as 36 | /// lower case characters and are case insensitive on input. 37 | /// 38 | /// RFC 4122 specifies output as lower case characters 39 | /// when NSUUID decodes the bytes back into the string it 40 | /// keeps them as upper case. We intentionally force 41 | /// the values back to lower case to keep in line with the RFC 42 | /// 43 | /// - SeeAlso: 44 | /// 45 | /// RFC 4122 (https://www.ietf.org/rfc/rfc4122.txt) 46 | /// 47 | /// Declaration of syntactic structure 48 | /// 49 | public override func deserialize(value: AnyObject?) -> AnyObject?{ 50 | guard let value = value as? NSData else { 51 | return nil 52 | } 53 | 54 | var bytes = [UInt8](count: 16, repeatedValue: 0) 55 | value.getBytes(&bytes, length: bytes.count) 56 | 57 | let uuid = NSUUID(UUIDBytes: bytes).UUIDString.lowercaseString 58 | return uuid 59 | } 60 | } 61 | 62 | 63 | public class CharField: Column{ 64 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 65 | self.init(label, type: .StringAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 66 | } 67 | } 68 | 69 | 70 | public class BooleanField: Column{ 71 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 72 | self.init(label, type: .BooleanAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 73 | } 74 | } 75 | 76 | 77 | public class IntegerField: Column{ 78 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 79 | self.init(label, type: .Integer64AttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 80 | } 81 | 82 | override public func modelValue(model: AmigoModel) -> AnyObject? { 83 | let value = model.valueForKey(label) 84 | 85 | if let value = value as? Int where value == 0 && primaryKey == true{ 86 | return nil 87 | } 88 | 89 | return value 90 | } 91 | } 92 | 93 | 94 | public class FloatField: Column{ 95 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 96 | self.init(label, type: .FloatAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 97 | } 98 | } 99 | 100 | 101 | public class DoubleField: Column{ 102 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 103 | self.init(label, type: .DoubleAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 104 | } 105 | } 106 | 107 | 108 | public class BinaryField: Column{ 109 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 110 | self.init(label, type: .BinaryDataAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 111 | } 112 | } 113 | 114 | 115 | public class DateTimeField: Column{ 116 | public convenience init(_ label: String, primaryKey: Bool = false, indexed: Bool = false, optional: Bool = true, unique: Bool = false, defaultValue: (()-> AnyObject?)? = nil) { 117 | self.init(label, type: .DateAttributeType, primaryKey: primaryKey, indexed: indexed, optional: optional, unique: unique, defaultValue: defaultValue) 118 | } 119 | } -------------------------------------------------------------------------------- /AmigoTests/ModelMappingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelMappingTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/31/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Amigo 11 | 12 | 13 | class ModelMappingTests: XCTestCase { 14 | var amigo: Amigo! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | super.tearDown() 23 | } 24 | 25 | func testOneToMany() { 26 | let dog = ORMModel(Dog.self, 27 | Column("id", type: Int.self, primaryKey: true), 28 | Column("label", type: String.self), 29 | OneToMany("people", using: People.self) 30 | ) 31 | 32 | let people = ORMModel(People.self, 33 | Column("id", type: Int.self, primaryKey: true), 34 | Column("label", type: String.self), 35 | Column("dog", type: ForeignKey(dog)) 36 | ) 37 | 38 | let engine = SQLiteEngineFactory(":memory:", echo: true) 39 | amigo = Amigo([dog, people], factory: engine) 40 | amigo.createAll() 41 | 42 | let session = amigo.session 43 | 44 | let d1 = Dog() 45 | d1.label = "Lucy" 46 | 47 | let p1 = People() 48 | p1.label = "Foo" 49 | p1.dog = d1 50 | 51 | let p2 = People() 52 | p2.label = "Bar" 53 | p2.dog = d1 54 | 55 | session.add([d1, p1, p2]) 56 | 57 | let results = session 58 | .query(People) 59 | .using(d1) 60 | .relationship("people") 61 | .all() 62 | 63 | XCTAssertEqual(results.count, 2) 64 | 65 | } 66 | 67 | func testManyToMany() { 68 | let parent = ORMModel(Parent.self, 69 | Column("id", type: Int.self, primaryKey: true), 70 | Column("label", type: String.self), 71 | ManyToMany("children", using: Child.self) 72 | ) 73 | 74 | let child = ORMModel(Child.self, 75 | Column("id", type: Int.self, primaryKey: true), 76 | Column("label", type: String.self), 77 | 78 | // if you would like to act on the inverse 79 | // define it as well. 80 | ManyToMany("parents", using: Parent.self) 81 | ) 82 | 83 | let engine = SQLiteEngineFactory(":memory:", echo: true) 84 | amigo = Amigo([parent, child], factory: engine) 85 | amigo.createAll() 86 | 87 | let session = amigo.session 88 | 89 | let p1 = Parent() 90 | p1.label = "Foo" 91 | 92 | let p2 = Parent() 93 | p2.label = "Foo" 94 | 95 | let c1 = Child() 96 | c1.label = "Baz" 97 | 98 | let c2 = Child() 99 | c2.label = "Qux" 100 | 101 | session.add([p1, p2, c1, c2]) 102 | 103 | // add 2 children to p1 104 | session.using(p1).relationship("children").add(c1, c2) 105 | 106 | var results = session 107 | .query(Child) 108 | .using(p1) 109 | .relationship("children") 110 | .all() 111 | 112 | XCTAssertEqual(results.count, 2) 113 | 114 | results = session 115 | .query(Child) 116 | .using(p2) 117 | .relationship("children") 118 | .all() 119 | 120 | XCTAssertEqual(results.count, 0) 121 | 122 | // add a shared parent to the children using 123 | // the inverse relationship to ensure it works as well 124 | session.using(c1).relationship("parents").add(p2) 125 | session.using(c2).relationship("parents").add(p2) 126 | 127 | 128 | results = session 129 | .query(Child) 130 | .using(p2) 131 | .relationship("children") 132 | .all() 133 | 134 | XCTAssertEqual(results.count, 2) 135 | 136 | } 137 | 138 | func testManyToManyThrough() { 139 | let workout = ORMModel(Workout.self, 140 | Column("id", type: Int.self, primaryKey: true), 141 | Column("label", type: String.self), 142 | ManyToMany("exercises", using: WorkoutExercise.self, throughModel: WorkoutMeta.self) 143 | ) 144 | 145 | let workoutExercise = ORMModel(WorkoutExercise.self, 146 | Column("id", type: Int.self, primaryKey: true), 147 | Column("label", type: String.self) 148 | ) 149 | 150 | let workoutMeta = ORMModel(WorkoutMeta.self, 151 | Column("id", type: Int.self, primaryKey: true), 152 | Column("duration", type: Int.self), 153 | Column("position", type: Int.self), 154 | Column("exercise", type: ForeignKey(workoutExercise)), 155 | Column("workout", type: ForeignKey(workout)) 156 | ) 157 | 158 | let engine = SQLiteEngineFactory(":memory:", echo: true) 159 | amigo = Amigo([workout, workoutExercise, workoutMeta], factory: engine) 160 | amigo.createAll() 161 | 162 | let session = amigo.session 163 | 164 | let w1 = Workout() 165 | w1.label = "foo" 166 | 167 | let e1 = WorkoutExercise() 168 | e1.label = "Jumping Jacks" 169 | 170 | let m1 = WorkoutMeta() 171 | m1.workout = w1 172 | m1.exercise = e1 173 | m1.duration = 60000 174 | m1.position = 1 175 | 176 | session.add([w1, e1, m1]) 177 | 178 | 179 | let results = session 180 | .query(WorkoutMeta) 181 | .using(w1) 182 | .relationship("exercises") 183 | .orderBy("position", ascending: true) 184 | .all() 185 | 186 | XCTAssertEqual(results.count, 1) 187 | 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Amigo.xcodeproj/xcshareddata/xcschemes/Amigo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 44 | 46 | 47 | 49 | 50 | 52 | 53 | 55 | 56 | 58 | 59 | 61 | 62 | 64 | 65 | 67 | 68 | 70 | 71 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 102 | 103 | 109 | 110 | 111 | 112 | 115 | 116 | 117 | 118 | 119 | 120 | 126 | 127 | 133 | 134 | 135 | 136 | 138 | 139 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/setup/orm_models.rst: -------------------------------------------------------------------------------- 1 | ORM Model Mapping 2 | =================================== 3 | 4 | 5 | Amigo can parse a :code:`NSManagedObjectModel` but all it's doing is 6 | converting the :code:`NSEntityDescriptions` into :code:`ORMModel` 7 | instances. Lets take a look at how we do that. 8 | 9 | 10 | .. important:: 11 | When performing a model mapping your data models **MUST** 12 | inherit from :code:`Amigo.AmigoModel` 13 | 14 | 15 | .. code-block:: swift 16 | 17 | import Amigo 18 | 19 | class Dog: AmigoModel{ 20 | dynamic var id: NSNumber! 21 | dynamic var label: String! 22 | } 23 | 24 | let dog = ORMModel(Dog.self, 25 | Column("id", type: Int.self, primaryKey: true) 26 | Column("label", type: String.self) 27 | Index("dog_label_idx", "label") 28 | ) 29 | 30 | // you could achieve the same mapping this way: 31 | 32 | let dog = ORMModel(Dog.self, 33 | Column("id", type: Int.self, primaryKey: true) 34 | Column("label", type: String.self, indexed: true) 35 | ) 36 | 37 | // now initialize Amigo 38 | // specifying 'echo: true' will have amigo print out 39 | // all of the SQL commands it's generating. 40 | let engine = SQLiteEngineFactory(":memory:", echo: true) 41 | amigo = Amigo([dog], factory: engine) 42 | amigo.createAll() 43 | 44 | 45 | Column Options 46 | ------------------------ 47 | 48 | Columns can be initialized with the following options (default values presented): 49 | 50 | .. code-block:: swift 51 | 52 | type: // See Column Types below 53 | primaryKey: Bool = false 54 | indexed: Bool = false 55 | optional: Bool = true 56 | unique: Bool = false 57 | 58 | 59 | Column Types 60 | ------------------------ 61 | 62 | Your avavilable options for `Column` types are as follows: 63 | 64 | .. code-block:: swift 65 | 66 | NSString 67 | String 68 | Int16 69 | Int32 70 | Int64 71 | Int 72 | NSDate 73 | NSData 74 | NSDecimalNumber 75 | Double 76 | Float 77 | Bool 78 | 79 | These effectvely map to the following :code:`NSAttributeType` 80 | found in :code:`CoreData` which you may also use for your column initialization: 81 | 82 | .. code-block:: swift 83 | 84 | NSAttributeType.StringAttributeType 85 | NSAttributeType.Integer16AttributeType 86 | NSAttributeType.Integer32AttributeType 87 | NSAttributeType.Integer64AttributeType 88 | NSAttributeType.DateAttributeType 89 | NSAttributeType.BinaryDataAttributeType 90 | NSAttributeType.DecimalAttributeType 91 | NSAttributeType.DoubleAttributeType 92 | NSAttributeType.FloatAttributeType 93 | NSAttributeType.BooleanAttributeType 94 | NSAttributeType.UndefinedAttributeType 95 | 96 | 97 | See the initializers in: 98 | 99 | https://github.com/blitzagency/amigo-swift/blob/master/Amigo/Column.swift 100 | 101 | 102 | One additional type exists for Column initialization and that's :code:`Amigo.ForeignKey` 103 | 104 | 105 | ForeignKeys 106 | ------------------- 107 | 108 | Amigo allows you to make Foreign Key Relationships. You can do though through 109 | the Managed Object Model or manually. 110 | 111 | In the Managed Object Model, ForeignKeys are represented by a **Relationship** 112 | that has a type of :code:`To One`. That gets translated to the :code:`ORMModel` 113 | mapping as follows: 114 | 115 | .. code-block:: swift 116 | 117 | 118 | import Amigo 119 | 120 | class Dog: AmigoModel{ 121 | dynamic var id: NSNumber! 122 | dynamic var label: String! 123 | } 124 | 125 | class Person: AmigoModel{ 126 | dynamic var id: NSNumber! 127 | dynamic var label: String! 128 | dynamic var dog: Dog! 129 | } 130 | 131 | 132 | let dog = ORMModel(Dog.self, 133 | Column("id", type: Int.self, primaryKey: true) 134 | Column("label", type: String.self) 135 | ) 136 | 137 | // You can use the ORMModel 138 | let person = ORMModel(Person.self, 139 | Column("id", type: Int.self, primaryKey: true) 140 | Column("label", type: String.self) 141 | Column("dog", type: ForeignKey(dog)) 142 | ) 143 | 144 | 145 | **OR** using the column itself: 146 | 147 | .. code-block:: swift 148 | 149 | // OR you can use the column: 150 | let person = ORMModel(Person.self, 151 | Column("id", type: Int.self, primaryKey: true) 152 | Column("label", type: String.self) 153 | Column("dog", type: ForeignKey(dog.table.c["id"])) 154 | ) 155 | 156 | 157 | 158 | One To Many 159 | ------------------- 160 | 161 | Using our :code:`Person/Dog` example above, we can also represent a 162 | One To Many relationship. 163 | 164 | In the case of a Managed Object Model, a One To Many is represented by a 165 | **Relationship** that has a type on :code:`To One` on one side and 166 | :code:`To Many` on the other side, aka the inverse relationship. 167 | 168 | In code it would look like this: 169 | 170 | 171 | .. code-block:: swift 172 | 173 | import Amigo 174 | 175 | class Dog: AmigoModel{ 176 | dynamic var id: NSNumber! 177 | dynamic var label: String! 178 | } 179 | 180 | class Person: AmigoModel{ 181 | dynamic var id: NSNumber! 182 | dynamic var label: String! 183 | dynamic var dog: Dog! 184 | } 185 | 186 | let dog = ORMModel(Dog.self, 187 | Column("id", type: Int.self, primaryKey: true), 188 | Column("label", type: String.self), 189 | OneToMany("people", using: Person.self, on: "dog") 190 | ) 191 | 192 | let person = ORMModel(Person.self, 193 | Column("id", type: Int.self, primaryKey: true), 194 | Column("label", type: String.self), 195 | Column("dog", type: ForeignKey(dog)) 196 | ) 197 | 198 | // specifying 'echo: true' will have amigo print out 199 | // all of the SQL commands it's generating. 200 | let engine = SQLiteEngineFactory(":memory:", echo: true) 201 | amigo = Amigo([dog, person], factory: engine) 202 | amigo.createAll() 203 | 204 | 205 | We can then query the One To Many Relationship this way: 206 | 207 | .. code-block:: swift 208 | 209 | let session = amigo.session 210 | 211 | let d1 = Dog() 212 | d1.label = "Lucy" 213 | 214 | let p1 = People() 215 | p1.label = "Foo" 216 | p1.dog = d1 217 | 218 | let p2 = People() 219 | p2.label = "Bar" 220 | p2.dog = d1 221 | 222 | session.add(d1, p1, p2) 223 | 224 | var results = session 225 | .query(People) 226 | .using(d1) 227 | .relationship("people") 228 | .all() 229 | 230 | Many To Many 231 | ------------------- 232 | -------------------------------------------------------------------------------- /Amigo/Select.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Select.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/7/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public func ==(lhs: Join, rhs: Join) -> Bool{ 12 | return lhs.hashValue == rhs.hashValue 13 | } 14 | 15 | public func ==(lhs: Select, rhs: Select) -> Bool{ 16 | return lhs.hashValue == rhs.hashValue 17 | } 18 | 19 | 20 | public protocol FromClause: Hashable, CustomStringConvertible{ 21 | 22 | } 23 | 24 | extension Table { 25 | public func join(table: Table) -> Join{ 26 | 27 | let joins = Array(self.columns.values) 28 | .filter{ $0.foreignKey?.relatedColumn == table.primaryKey } 29 | .map{ column -> Join in 30 | let fk = column.foreignKey! 31 | let format = "\(self.label).\(fk.column.label) = \(table.label).\(table.primaryKey!.label)" 32 | return self.join(table, on: format) 33 | } 34 | 35 | return joins[0] 36 | } 37 | } 38 | 39 | extension FromClause{ 40 | 41 | public var hashValue: Int{ 42 | return description.hashValue 43 | } 44 | 45 | // public func join(table: Table) -> Join{ 46 | // guard let current = self as? Table else{ 47 | // fatalError("Cannot join from non-table: \(self)") 48 | // } 49 | // 50 | // let format = "\(current.label).\(current.primaryKey!.label) = \(table.label).\(table.primaryKey!.label)" 51 | // return self.join(table, on: format) 52 | // } 53 | 54 | public func join(table: Table, on: String, _ args: AnyObject...) -> Join{ 55 | return self.join(table, on: on, args: args) 56 | } 57 | 58 | public func join(table: Table, on: String, args: [AnyObject]?) -> Join{ 59 | guard let current = self as? Table else{ 60 | fatalError("Cannot join from non-table: \(self)") 61 | } 62 | 63 | return Join(current, right: table, on: on) 64 | } 65 | 66 | public func select() -> Select{ 67 | if let table = self as? Table{ 68 | return Select(table) 69 | } 70 | 71 | fatalError("Cannot select from non-table: \(self)") 72 | } 73 | 74 | public func insert() -> Insert{ 75 | if let table = self as? Table{ 76 | return Insert(table) 77 | } 78 | 79 | fatalError("Cannot insert from non-table: \(self)") 80 | } 81 | 82 | public func insert(upsert upsert: Bool) -> Insert{ 83 | if let table = self as? Table{ 84 | return Insert(table, upsert: upsert) 85 | } 86 | 87 | fatalError("Cannot insert from non-table: \(self)") 88 | } 89 | 90 | 91 | public func update() -> Update{ 92 | if let table = self as? Table{ 93 | return Update(table) 94 | } 95 | 96 | fatalError("Cannot update from non-table: \(self)") 97 | } 98 | 99 | public func delete() -> Delete{ 100 | if let table = self as? Table{ 101 | return Delete(table) 102 | } 103 | 104 | fatalError("Cannot delete from non-table: \(self)") 105 | } 106 | } 107 | 108 | 109 | public class Join: FromClause{ 110 | public let left: Table 111 | public let right: Table 112 | public let on: String 113 | 114 | public init(_ left: Table, right: Table, on: String){ 115 | self.left = left 116 | self.right = right 117 | self.on = on 118 | } 119 | 120 | public var description: String{ 121 | return "" 122 | } 123 | } 124 | 125 | 126 | public class Select: FromClause, Filterable{ 127 | 128 | public let columns: [Column] 129 | // swift doesn't have an ordered set in it's stdlib so 130 | // we fake it 131 | public var from = [AnyObject]() 132 | public var predicate: String? 133 | public var predicateParams: [AnyObject]? 134 | 135 | var hasFrom = [String: AnyObject]() 136 | var _limit: Int? 137 | var _offset: Int? 138 | var _orderBy = [OrderBy]() 139 | 140 | public convenience init(_ tables: Table...){ 141 | self.init(tables) 142 | } 143 | 144 | public convenience init(_ tables:[Table]){ 145 | let columns = tables.map{$0.sortedColumns}.flatMap{$0} 146 | self.init(columns) 147 | 148 | tables.forEach(appendFrom) 149 | } 150 | 151 | public convenience init(_ columns: Column...){ 152 | self.init(columns) 153 | } 154 | 155 | public init(_ columns: [Column]){ 156 | self.columns = columns 157 | } 158 | 159 | public func appendFrom(obj: T){ 160 | let desc = obj.description 161 | // this isn't a great algorithim for the speed of things 162 | // it would be better if we had an ordered set for this. 163 | 164 | if let join = obj as? Join{ 165 | if let table = hasFrom[join.right.description] as? Table{ 166 | for (index, each) in from.enumerate(){ 167 | if let candidate = each as? Table{ 168 | if candidate == table { 169 | from.removeAtIndex(index) 170 | break 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | if let _ = hasFrom[desc]{ 179 | return 180 | } 181 | 182 | let target = obj as! AnyObject 183 | 184 | hasFrom[desc] = target 185 | from.append(target) 186 | } 187 | 188 | public func selectFrom(from: T...) -> Select{ 189 | from.forEach(appendFrom) 190 | return self 191 | } 192 | 193 | // public func selectFrom(from: [T]...) -> Select{ 194 | // from.map(selectFrom) 195 | // return self 196 | // } 197 | 198 | public func selectFrom(from: [T]) -> Select{ 199 | from.forEach(appendFrom) 200 | return self 201 | } 202 | 203 | public func limit(value: Int) -> Select{ 204 | _limit = value 205 | return self 206 | } 207 | 208 | public func offset(value: Int) -> Select{ 209 | _offset = value 210 | return self 211 | } 212 | 213 | public func orderBy(value: OrderBy...) -> Select{ 214 | return orderBy(value) 215 | } 216 | 217 | public func orderBy(value: [OrderBy]) -> Select{ 218 | _orderBy.appendContentsOf(value) 219 | return self 220 | } 221 | 222 | public var description: String{ 223 | let names = columns.map{"\($0.table!.label).\($0.label)"} 224 | let out = names.joinWithSeparator(" ") 225 | return "" 226 | } 227 | } -------------------------------------------------------------------------------- /docs/querying/index.rst: -------------------------------------------------------------------------------- 1 | Querying 2 | =================================== 3 | 4 | Querying with :code:`Amigo` is similar to querying with Django or 5 | SQLAlchemy. Lets run though a few examples. In each of the following 6 | examples we will assume we have already done our model mapping 7 | and we have an :code:`amigo` instance available to us. For 8 | more information on model mapping see: :doc:`/models/index` 9 | 10 | 11 | Get an object by id 12 | --------------------- 13 | 14 | :code:`get` returns an optional model as it may fail if the id 15 | is not present. 16 | 17 | .. code-block:: swift 18 | 19 | let session = amigo.session 20 | let maybeDog: Dog? = session.query(Dog).get(1) 21 | 22 | 23 | Get all objects 24 | ---------------------------------- 25 | 26 | :code:`all` 27 | 28 | .. code-block:: swift 29 | 30 | let session = amigo.session 31 | let dogs: [Dog] = session.query(Dog).all() 32 | 33 | 34 | Order objects 35 | ---------------------------------- 36 | 37 | :code:`orderBy` 38 | 39 | .. code-block:: swift 40 | 41 | let session = amigo.session 42 | let dogs: [Dog] = session 43 | .query(Dog) 44 | .orderBy("label", ascending: false) // by default ascending is true 45 | .all() 46 | 47 | 48 | Filter objects 49 | ---------------------------------- 50 | 51 | :code:`filter` 52 | 53 | .. code-block:: swift 54 | 55 | let session = amigo.session 56 | let dogs: [Dog] = session 57 | .query(Dog) 58 | .filter("id > 3") 59 | .all() 60 | 61 | .. note :: 62 | 63 | Filter strings are converted into a :code:`NSPredicate` behind the 64 | scenes. When using the :code:`SQLiteEngine`, the constant params are 65 | extracted and replaced with `?` in generated query. The params are 66 | then passed to FMDB for escaping/replacement. 67 | 68 | 69 | Limit objects 70 | ---------------------------------- 71 | 72 | :code:`limit` 73 | 74 | .. code-block:: swift 75 | 76 | let session = amigo.session 77 | let dogs: [Dog] = session 78 | .query(Dog) 79 | .limit(10) 80 | .all() 81 | 82 | 83 | 84 | Offset objects 85 | ---------------------------------- 86 | 87 | :code:`offset` 88 | 89 | .. code-block:: swift 90 | 91 | let session = amigo.session 92 | let dogs: [Dog] = session 93 | .query(Dog) 94 | .limit(10) 95 | .offset(5) 96 | .all() 97 | 98 | 99 | Full foreign key in one query (aka JOIN) 100 | ---------------------------------------- 101 | 102 | :code:`selectRelated` 103 | 104 | See :ref:`foreign-key` for more. 105 | 106 | .. code-block:: swift 107 | 108 | let dog = ORMModel(Dog.self, 109 | Column("id", type: Int.self, primaryKey: true) 110 | Column("label", type: String.self) 111 | ) 112 | 113 | // You can use the ORMModel 114 | let person = ORMModel(Person.self, 115 | Column("id", type: Int.self, primaryKey: true) 116 | Column("label", type: String.self) 117 | Column("dog", type: ForeignKey(dog)) 118 | ) 119 | 120 | // specifying 'echo: true' will have amigo print out 121 | // all of the SQL commands it's generating. 122 | let engine = SQLiteEngineFactory(":memory:", echo: true) 123 | amigo = Amigo([dog, person], factory: engine) 124 | amigo.createAll() 125 | 126 | let session = amigo.session 127 | 128 | let d1 = Dog() 129 | d1.label = "Lucy" 130 | 131 | let p1 = Person() 132 | p1.label = "Foo" 133 | p1.dog = d1 134 | 135 | session.add(d1, p1) 136 | 137 | let result = session 138 | .query(Person) 139 | .selectRelated("dog") 140 | .all() 141 | 142 | 143 | Filter and Order By related fields 144 | ----------------------------------- 145 | 146 | :code:`filter` 147 | :code:`orderBy` 148 | 149 | .. code-block:: swift 150 | 151 | let dog = ORMModel(Dog.self, 152 | Column("id", type: Int.self, primaryKey: true) 153 | Column("label", type: String.self) 154 | ) 155 | 156 | // You can use the ORMModel 157 | let person = ORMModel(Person.self, 158 | Column("id", type: Int.self, primaryKey: true) 159 | Column("label", type: String.self) 160 | Column("dog", type: ForeignKey(dog)) 161 | ) 162 | 163 | // specifying 'echo: true' will have amigo print out 164 | // all of the SQL commands it's generating. 165 | let engine = SQLiteEngineFactory(":memory:", echo: true) 166 | amigo = Amigo([dog, person], factory: engine) 167 | amigo.createAll() 168 | 169 | let session = amigo.session 170 | 171 | let d1 = Dog() 172 | d1.label = "Lucy" 173 | 174 | let p1 = Person() 175 | p1.label = "Foo" 176 | p1.dog = d1 177 | 178 | session.add(d1, p1) 179 | 180 | let result = session 181 | .query(Person) 182 | .selectRelated("dog") 183 | .filter("id > 1 AND dog.id > 1") // note the dot notation 184 | .orderBy("dog.id", ascending: false) // note the dot notation 185 | .all() 186 | 187 | 188 | One-To-Many Query 189 | ----------------------------------- 190 | 191 | :code:`relationship` 192 | 193 | See :ref:`one-to-many` for the full example. 194 | 195 | .. code-block:: swift 196 | 197 | let session = amigo.session 198 | var results = session 199 | .query(People) // We want the People objects 200 | .using(d1) // by using the d1 (Dog) object 201 | .relationship("people") // and following the d1 model's "people" relationship 202 | .all() 203 | 204 | 205 | 206 | Many-To-Many Query 207 | ----------------------------------- 208 | 209 | :code:`relationship` 210 | 211 | See :ref:`many-to-many` for the full example. 212 | 213 | .. code-block:: swift 214 | 215 | let session = amigo.session 216 | var results = session 217 | .query(Child) // We want the Child objects 218 | .using(p1) // by using the p1 (Parent) object 219 | .relationship("children") // and following the d1 model's "children" relationship 220 | .all() 221 | 222 | 223 | Many-To-Many With Through Models Query 224 | --------------------------------------- 225 | 226 | :code:`relationship` 227 | 228 | See :ref:`many-to-many-through-models` for the full example. 229 | 230 | .. code-block:: swift 231 | 232 | let session = amigo.session 233 | 234 | var results = session 235 | .query(WorkoutMeta) // We want the WorkoutMeta objects 236 | .using(w1) // by using the w1 (Workout) object 237 | .relationship("exercises") // and following the w1 model's "exercises" relationship 238 | .orderBy("position", ascending: true) // order the results by WorkoutMeta.position ascending 239 | .all() 240 | 241 | -------------------------------------------------------------------------------- /AmigoTests/AmigoQuerySetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AmigoQuerySetTests.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 7/24/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import CoreData 11 | import Amigo 12 | 13 | class AmigoQuerySetTests: AmigoTestBase { 14 | 15 | 16 | func testMultipleForeignKeys(){ 17 | let session = amigo.session 18 | 19 | let d1 = Dog() 20 | d1.label = "Lucy" 21 | 22 | let c1 = Cat() 23 | c1.label = "Ollie" 24 | 25 | let p1 = People() 26 | p1.label = "Foo" 27 | p1.dog = d1 28 | p1.cat = c1 29 | 30 | session.add(p1) 31 | 32 | let people = session 33 | .query(People) 34 | .selectRelated("dog", "cat") 35 | .all() 36 | 37 | XCTAssertEqual(people.count, 1) 38 | XCTAssertNotNil(people[0].dog) 39 | XCTAssertNotNil(people[0].cat) 40 | XCTAssertEqual(people[0].dog.label, "Lucy") 41 | XCTAssertEqual(people[0].cat.label, "Ollie") 42 | } 43 | 44 | func testOrderByDifferentTable(){ 45 | let session = amigo.session 46 | 47 | let d1 = Dog() 48 | d1.label = "Lucy" 49 | 50 | let d2 = Dog() 51 | d2.label = "Ollie" 52 | 53 | let p1 = People() 54 | p1.label = "Foo" 55 | p1.dog = d1 56 | 57 | let p2 = People() 58 | p2.label = "Bar" 59 | p2.dog = d2 60 | 61 | session.add(p1) 62 | session.add(p2) 63 | 64 | let people = session 65 | .query(People) 66 | .selectRelated("dog") 67 | .orderBy("dog.label", ascending: false) 68 | .all() 69 | 70 | XCTAssertEqual(people.count, 2) 71 | XCTAssertEqual(people[0].id, 2) 72 | XCTAssertEqual(people[0].dog.label, "Ollie") 73 | } 74 | 75 | func testFilterBy(){ 76 | let session = amigo.session 77 | 78 | let d1 = Dog() 79 | d1.label = "Lucy" 80 | 81 | let d2 = Dog() 82 | d2.label = "Ollie" 83 | 84 | let p1 = People() 85 | p1.label = "Foo" 86 | p1.dog = d1 87 | 88 | let p2 = People() 89 | p2.label = "Bar" 90 | p2.dog = d2 91 | 92 | session.add(p1) 93 | session.add(p2) 94 | 95 | let people = session 96 | .query(People) 97 | .filter("label = 'Foo'") 98 | .all() 99 | 100 | XCTAssertEqual(people.count, 1) 101 | XCTAssertEqual(people[0].id, 1) 102 | } 103 | 104 | func testFilterByDifferentTable(){ 105 | let session = amigo.session 106 | 107 | let d1 = Dog() 108 | d1.label = "Lucy" 109 | 110 | let d2 = Dog() 111 | d2.label = "Ollie" 112 | 113 | let p1 = People() 114 | p1.label = "Foo" 115 | p1.dog = d1 116 | 117 | let p2 = People() 118 | p2.label = "Bar" 119 | p2.dog = d2 120 | 121 | session.add(p1) 122 | session.add(p2) 123 | 124 | let people = session 125 | .query(People) 126 | .selectRelated("dog") 127 | .orderBy("dog.label", ascending: false) 128 | .filter("dog.label = \"Lucy\"") 129 | .all() 130 | 131 | XCTAssertEqual(people.count, 1) 132 | XCTAssertEqual(people[0].id, 1) 133 | XCTAssertEqual(people[0].dog.label, "Lucy") 134 | } 135 | 136 | func testOrderBySameTable(){ 137 | let session = amigo.session 138 | 139 | let d1 = Dog() 140 | d1.label = "Lucy" 141 | 142 | let d2 = Dog() 143 | d2.label = "Ollie" 144 | 145 | session.add(d1) 146 | session.add(d2) 147 | 148 | var dogs = session 149 | .query(Dog) 150 | .orderBy("label", ascending: false) 151 | .all() 152 | 153 | XCTAssertEqual(dogs.count, 2) 154 | XCTAssertEqual(dogs[0].id, 2) 155 | XCTAssertEqual(dogs[0].label, "Ollie") 156 | 157 | dogs = session 158 | .query(Dog) 159 | .orderBy("label") 160 | .all() 161 | 162 | XCTAssertEqual(dogs.count, 2) 163 | XCTAssertEqual(dogs[0].id, 1) 164 | XCTAssertEqual(dogs[0].label, "Lucy") 165 | } 166 | 167 | func testForeignKeyAutoSave(){ 168 | let session = amigo.session 169 | 170 | let d1 = Dog() 171 | d1.label = "Lucy" 172 | 173 | let p1 = People() 174 | p1.label = "Ollie Cat" 175 | p1.dog = d1 176 | 177 | var candidate = session.query(Dog).get(1) 178 | XCTAssertNil(candidate) 179 | 180 | session.add(p1) 181 | 182 | candidate = session.query(Dog).get(1) 183 | XCTAssertNotNil(candidate) 184 | } 185 | 186 | func testSelectRelated(){ 187 | let session = amigo.session 188 | 189 | let d1 = Dog() 190 | d1.label = "Lucy" 191 | 192 | let p1 = People() 193 | p1.label = "Ollie Cat" 194 | p1.dog = d1 195 | 196 | session.add(p1) 197 | 198 | var person = session.query(People).get(1)! 199 | XCTAssertNil(person.dog) 200 | 201 | person = session 202 | .query(People) 203 | .selectRelated("dog") 204 | .get(1)! 205 | 206 | XCTAssertNotNil(person.dog) 207 | XCTAssertEqual(person.dog.label, "Lucy") 208 | } 209 | 210 | func testLimit(){ 211 | let session = amigo.session 212 | 213 | let a1 = Author() 214 | a1.firstName = "Lucy" 215 | a1.lastName = "Dog" 216 | 217 | let a2 = Author() 218 | a2.firstName = "Ollie" 219 | a2.lastName = "Cat" 220 | 221 | session.add(a1) 222 | session.add(a2) 223 | 224 | let authors = session 225 | .query(Author) 226 | .limit(1) 227 | .all() 228 | 229 | XCTAssertEqual(authors.count, 1) 230 | XCTAssertEqual(authors[0].firstName, "Lucy") 231 | XCTAssertEqual(authors[0].lastName, "Dog") 232 | } 233 | 234 | func testOffset(){ 235 | let session = amigo.session 236 | 237 | let a1 = Author() 238 | a1.firstName = "Lucy" 239 | a1.lastName = "Dog" 240 | 241 | let a2 = Author() 242 | a2.firstName = "Ollie" 243 | a2.lastName = "Cat" 244 | 245 | session.add(a1) 246 | session.add(a2) 247 | 248 | let authors = session 249 | .query(Author) 250 | .limit(1) 251 | .offset(1) 252 | .all() 253 | 254 | XCTAssertEqual(authors.count, 1) 255 | XCTAssertEqual(authors[0].firstName, "Ollie") 256 | XCTAssertEqual(authors[0].lastName, "Cat") 257 | } 258 | 259 | func testOneToMany(){ 260 | 261 | let session = amigo.session 262 | 263 | let a1 = Author() 264 | a1.firstName = "Lucy" 265 | a1.lastName = "Dog" 266 | 267 | let a2 = Author() 268 | a2.firstName = "Ollie" 269 | a2.lastName = "Cat" 270 | 271 | let p1 = Post() 272 | p1.title = "The Story of Barking" 273 | p1.author = a1 274 | 275 | let p2 = Post() 276 | p2.title = "10 Things You Should Know When Chasing Squirrels" 277 | p2.author = a1 278 | 279 | let p3 = Post() 280 | p3.title = "The Story of Being a Cat" 281 | p3.author = a2 282 | 283 | session.add(a1) 284 | session.add(a2) 285 | session.add(p1) 286 | session.add(p2) 287 | session.add(p3) 288 | 289 | let posts = session.query(Post) 290 | .using(a1) 291 | .relationship("posts") 292 | .all() 293 | 294 | XCTAssertEqual(posts.count, 2) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /AmigoTests/App.xcdatamodeld/App.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Amigo/Amigo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Amigo.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 6/29/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | public class Amigo: AmigoConfigured{ 14 | 15 | public let config: AmigoConfiguration 16 | public let engineFactory: EngineFactory 17 | 18 | public func query(value: T.Type) -> QuerySet{ 19 | let model = typeIndex[value.description()]! 20 | return QuerySet(model: model, config: threadConfig()) 21 | } 22 | 23 | public func async(queue queue: dispatch_queue_t? = nil, action: () -> T) -> AmigoSessionAsyncAction{ 24 | 25 | if let queue = queue{ 26 | return AmigoSessionAsyncAction(action: action, queue: queue) 27 | } 28 | 29 | return AmigoSessionAsyncAction(action: action) 30 | } 31 | 32 | public func async(queue queue: dispatch_queue_t? = nil, action: () -> ()){ 33 | if let queue = queue{ 34 | AmigoSessionAsyncAction(action: action, queue: queue).run() 35 | return 36 | } 37 | 38 | AmigoSessionAsyncAction(action: action).run() 39 | } 40 | 41 | public var session: AmigoSession{ 42 | // there is currently an issue here with multiple threads. 43 | // a session could be grabbed from each thread, but in the current 44 | // model they are effectively using the same connection. 45 | // there will be many unintended consequences. 46 | // 47 | // it may be best here to use a new connection per session 48 | // we would need to ensure sqlite is compiled in multi-threaded mode: 49 | // https://www.sqlite.org/threadsafe.html 50 | // 51 | // for now lets just get things working so we are ignoring it. 52 | // in the case of FMDB, per the docs it's always been safe to make 53 | // a FMDatabase object per thread. 54 | 55 | 56 | let session = AmigoSession(config: threadConfig()) 57 | session.begin() 58 | return session 59 | } 60 | 61 | public convenience init(_ mom: NSManagedObjectModel, factory: EngineFactory, mapperType: AmigoEntityDescriptionMapper.Type = EntityDescriptionMapper.self){ 62 | let mapper = mapperType.init() 63 | let models = mom.sortedEntities().map(mapper.map) 64 | self.init(models, factory: factory, mapper: mapper) 65 | } 66 | 67 | public convenience init(_ models: [ORMModel], factory: EngineFactory, mapperType: Mapper.Type = DefaultMapper.self){ 68 | let mapper = mapperType.init() 69 | self.init(models, factory: factory, mapper: mapper) 70 | } 71 | 72 | public init(_ models: [ORMModel], factory: EngineFactory, mapper: Mapper){ 73 | var tableIndex = [String:ORMModel]() 74 | var typeIndex = [String:ORMModel]() 75 | 76 | models.forEach{ 77 | tableIndex[$0.table.label] = $0 78 | typeIndex[$0.type] = $0 79 | } 80 | 81 | engineFactory = factory 82 | 83 | self.config = AmigoConfiguration( 84 | engine: factory.connect(), 85 | mapper: mapper, 86 | tableIndex: tableIndex, 87 | typeIndex: typeIndex 88 | ) 89 | 90 | initializeOneToMany(models) 91 | initializeManyToMany(models) 92 | 93 | } 94 | 95 | func initializeOneToMany(models:[ORMModel]){ 96 | let relationships = models 97 | .map{Array($0.relationships.values)} 98 | .flatMap{$0} 99 | 100 | let o2m = relationships.filter{$0 is OneToMany}.map{$0 as! OneToMany} 101 | 102 | for each in o2m{ 103 | if each.column == nil{ 104 | let originModel = config.tableIndex[each.originTable]! 105 | let destinationModel = config.tableIndex[each.table]! 106 | 107 | for (key, value) in destinationModel.foreignKeys{ 108 | if value.foreignKey!.relatedTable == originModel.table{ 109 | each.column = key 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | func initializeManyToMany(models:[ORMModel]){ 117 | // find all the ManyToMany Relationships so we can inject tables. 118 | let relationships = models 119 | .map{Array($0.relationships.values)} 120 | .flatMap{$0} 121 | 122 | let m2m = relationships.filter{$0 is ManyToMany}.map{$0 as! ManyToMany} 123 | var m2mHash = [ManyToMany: [ManyToMany]]() 124 | var m2mThroughModels = [ManyToMany]() 125 | 126 | m2m.forEach{ (value: ManyToMany) -> Void in 127 | value.left = config.tableIndex[value.tables[0]]! 128 | value.right = config.tableIndex[value.tables[1]]! 129 | 130 | if var container = m2mHash[value]{ 131 | container.append(value) 132 | m2mHash[value] = container 133 | } else { 134 | let container = [value] 135 | m2mHash[value] = container 136 | } 137 | 138 | if let throughModel = value.throughModel{ 139 | let model = config.typeIndex[throughModel]! 140 | 141 | model.throughModelRelationship = value 142 | value.through = model 143 | 144 | Array(model.foreignKeys.values).forEach{ (c: Column) -> Void in 145 | if c.foreignKey!.relatedColumn == value.left.primaryKey || c.foreignKey!.relatedColumn == value.right.primaryKey{ 146 | c.optional = false 147 | } 148 | } 149 | 150 | m2mThroughModels.append(value) 151 | } 152 | } 153 | 154 | // ensure the throughModel is registered on both 155 | // sides of the relationship 156 | m2mThroughModels.forEach{ (value: ManyToMany) -> Void in 157 | m2mHash[value]?.forEach{ (each: ManyToMany) -> Void in 158 | each.throughModel = value.throughModel 159 | each.through = value.through 160 | } 161 | } 162 | 163 | // use the set to omit duplicates 164 | Set(m2m).forEach{ (value: ManyToMany) -> Void in 165 | let left = "\(value.left.label)_\(value.left.primaryKey!.label)" 166 | let right = "\(value.right.label)_\(value.right.primaryKey!.label)" 167 | 168 | var columns: [SchemaItem] = [ 169 | Column("id", type: Int.self, primaryKey: true), 170 | Column(left, type: ForeignKey(value.left.table), indexed: true, optional: false), 171 | Column(right, type: ForeignKey(value.right.table), indexed: true, optional: false) 172 | ] 173 | 174 | if let model = value.through{ 175 | let label = "\(model.label)_\(model.primaryKey!.label)" 176 | columns.append(Column(label, type: ForeignKey(model.table), indexed: true, optional: false)) 177 | } 178 | 179 | let table = Table(value.tableName, metadata: ORMModel.metadata, items: columns) 180 | 181 | m2mHash[value]?.forEach{ (value: ManyToMany) -> Void in 182 | value.associationTable = table 183 | } 184 | } 185 | } 186 | 187 | func threadConfig() -> AmigoConfiguration{ 188 | let obj = AmigoConfiguration( 189 | engine: engineFactory.connect(), 190 | mapper: config.mapper, 191 | tableIndex: config.tableIndex, 192 | typeIndex: config.typeIndex 193 | ) 194 | 195 | return obj 196 | } 197 | 198 | public func execute(sql: String, params: [AnyObject]! = nil){ 199 | config.engine.execute(sql, params: params) 200 | } 201 | 202 | public func execute(sql: String, params: [AnyObject]! = nil, mapper: Input -> Output) -> Output { 203 | return config.engine.execute(sql, params: params, mapper: mapper) 204 | } 205 | 206 | public func createAll(){ 207 | ORMModel.metadata.createAll(config.engine) 208 | } 209 | 210 | 211 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Amigo.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Amigo.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Amigo" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Amigo" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Amigo.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Amigo.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /Amigo/QuerySet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuerySet.swift 3 | // Amigo 4 | // 5 | // Created by Adam Venturella on 6/29/15. 6 | // Copyright © 2015 BLITZ. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | public class QuerySet: AmigoConfigured{ 13 | 14 | public let config: AmigoConfiguration 15 | public let model: ORMModel 16 | 17 | public var maybePredicate: NSPredicate? 18 | 19 | var _related = [String]() 20 | var _values = [String]() 21 | var _using : AmigoModel? 22 | var _relationship: String? 23 | var _limit: Int? 24 | var _offset: Int? 25 | var _orderBy = [OrderBy]() 26 | 27 | 28 | init(model: ORMModel, config: AmigoConfiguration){ 29 | self.config = config 30 | self.model = model 31 | } 32 | 33 | public func selectRelated(values: String...) -> QuerySet{ 34 | _related = values 35 | return self 36 | } 37 | 38 | public func relationship(value: String) -> QuerySet{ 39 | _relationship = value 40 | return self 41 | } 42 | 43 | public func values(values: String...) -> QuerySet{ 44 | _values = values 45 | return self 46 | } 47 | 48 | public func limit(value:Int) -> QuerySet{ 49 | _limit = value 50 | return self 51 | } 52 | 53 | public func offset(value:Int) -> QuerySet{ 54 | _offset = value 55 | return self 56 | } 57 | 58 | public func orderBy(key: String, ascending: Bool = true) -> QuerySet{ 59 | let order = ascending ? Asc(key) : Desc(key) as OrderBy 60 | _orderBy.append(order) 61 | return self 62 | } 63 | 64 | public func filter(format: String) -> QuerySet{ 65 | maybePredicate = NSPredicate(format: format) 66 | return self 67 | } 68 | 69 | public func using(model: U) -> QuerySet{ 70 | _using = model 71 | 72 | return self 73 | } 74 | 75 | public func get(primaryKey: AnyObject) -> T?{ 76 | self.filter("\(model.primaryKey.label) = \"\(primaryKey)\"") 77 | let results = self.all() 78 | return results.first 79 | } 80 | 81 | public func all() -> [T]{ 82 | return engine.execute(self) 83 | } 84 | 85 | public func count() -> Int{ 86 | // var error: NSError? 87 | // let request = fetchRequest 88 | // request.returnsObjectsAsFaults = true 89 | // 90 | // let count = context.countForFetchRequest(fetchRequest, error: &error) 91 | return 0 92 | } 93 | 94 | public func compile() -> Select { 95 | // if we are doing a selectRelated, we need to set all 96 | // of the columns 1st 97 | var joins = [Join]() 98 | var columns : [Column] 99 | var select: Select 100 | let explicitColumns: Bool 101 | let fromTable: Table 102 | 103 | if _values.count == 0 { 104 | explicitColumns = false 105 | columns = model.columns 106 | } else { 107 | explicitColumns = true 108 | columns = getExplicitColumns() 109 | } 110 | 111 | for each in _related{ 112 | if let fkColumn = model.foreignKeys[each]{ 113 | let relatedTable = fkColumn.foreignKey!.relatedColumn.table! 114 | let join = model.table.join(relatedTable) 115 | let joinColumns = tableIndex[relatedTable.label]!.columns 116 | 117 | if explicitColumns == false { 118 | columns = columns + joinColumns 119 | } 120 | 121 | joins.append(join) 122 | } 123 | } 124 | 125 | // for a many to many we change the table here to the 126 | // pivot table. and append two joins (select related) 127 | // to the selectFrom 128 | 129 | if let using = _using, 130 | let key = _relationship, 131 | let relatedModel = config.typeIndex[using.dynamicType.description()], 132 | let relationship = relatedModel.relationships[key] as? ManyToMany{ 133 | /* 134 | SELECT w.label, m.workout_id, m.id, m.type, m.count, e.label 135 | FROM workout_exercise AS we 136 | LEFT JOIN meta AS m ON m.id = we.meta_id 137 | LEFT JOIN exercise AS e ON e.id = we.exercise_id 138 | LEFT JOIN workout AS w ON w.id = we.workout_id 139 | ORDER BY we.workout_id 140 | */ 141 | 142 | select = Select(columns) 143 | .selectFrom(relationship.associationTable) 144 | 145 | if let through = relationship.through{ 146 | select.selectFrom(relationship.associationTable.join(through.table)) 147 | fromTable = through.table 148 | 149 | } else { 150 | let joinTarget = relationship.left == model ? 151 | relationship.associationTable.join(relationship.left.table) : 152 | relationship.associationTable.join(relationship.right.table) 153 | 154 | select.selectFrom(joinTarget) 155 | fromTable = relationship.associationTable 156 | } 157 | 158 | } else { 159 | fromTable = model.table 160 | select = Select(columns).selectFrom(model.table) 161 | } 162 | 163 | 164 | if let (filter, params) = compileFilter(fromTable){ 165 | select.filter(filter, params: params) 166 | } 167 | 168 | select.orderBy(_orderBy.map{ (value: OrderBy) -> OrderBy in 169 | let parts = value.keyPath.unicodeScalars.split{$0 == "."}.map(String.init) 170 | 171 | if parts.count == 1{ 172 | let name = model.table.c[parts[0]]!.qualifiedLabel! 173 | return value.dynamicType.init(name) 174 | } else if parts.count == 2{ 175 | let column = model.table.c[parts[0] + "_id"]! 176 | let table = column.foreignKey!.relatedColumn.table! 177 | let name = table.c[parts[1]]!.qualifiedLabel! 178 | 179 | return value.dynamicType.init(name) 180 | } 181 | 182 | return value.dynamicType.init(value.keyPath) 183 | }) 184 | 185 | if let limit = _limit{ 186 | select.limit(limit) 187 | } 188 | 189 | if let offset = _offset{ 190 | select.offset(offset) 191 | } 192 | 193 | if joins.count > 0 { 194 | select.selectFrom(joins) 195 | } 196 | 197 | return select 198 | } 199 | 200 | func compileFilter(fromTable: Table) -> (String, [AnyObject])?{ 201 | var predicate: NSPredicate? = maybePredicate 202 | 203 | 204 | if let using = _using, let key = _relationship, let relatedModel = config.typeIndex[using.dynamicType.description()]{ 205 | // there is gonna be a problem here. If 2 libs use "author" the 206 | // qualified names will be fine, but the dotted names 207 | // could have naming conflicts that can't be resolved 208 | // without a fully qualified name. 209 | if let relationship = relatedModel.relationships[key] as? OneToMany { 210 | let id = using.valueForKey(relatedModel.primaryKey!.label)! 211 | let relatedTable = fromTable.model!.foreignKeys[relationship.column]!.foreignKey!.relatedTable 212 | 213 | let suffix = relatedTable.primaryKey!.label 214 | let p = "\(relationship.column)_\(suffix) = \"\(id)\"" 215 | 216 | let relationshipPredicate = NSPredicate(format: p) 217 | 218 | if let filterPredicate = predicate{ 219 | predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [filterPredicate, relationshipPredicate]) 220 | } else { 221 | predicate = relationshipPredicate 222 | } 223 | } 224 | 225 | if let relationship = relatedModel.relationships[key] as? ManyToMany { 226 | let obj = relationship.left == relatedModel ? relationship.left : relationship.right 227 | let column = "\(obj.label)_\(obj.primaryKey!.label)" 228 | let id = using.valueForKey(obj.primaryKey!.label)! 229 | let p = "\(column) = \"\(id)\"" 230 | 231 | let relationshipPredicate = NSPredicate(format: p) 232 | 233 | if let filterPredicate = predicate{ 234 | predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [filterPredicate, relationshipPredicate]) 235 | } else { 236 | predicate = relationshipPredicate 237 | } 238 | } 239 | } 240 | 241 | if let predicate = predicate{ 242 | let (filter, params) = engine.compiler.compile(predicate, table: fromTable, models: config.tableIndex) 243 | return(filter, params) 244 | } else { 245 | return nil 246 | } 247 | 248 | } 249 | 250 | func getExplicitColumns() -> [Column]{ 251 | 252 | let cols = _values.map{ value -> Column in 253 | let parts = value.unicodeScalars.split{ $0 == "."}.map(String.init) 254 | 255 | if parts.count == 1{ 256 | return model.table.columns[parts[0]]! 257 | } else { 258 | // this is a FK Column. We append "_id" as the ORM hides 259 | // that idea from the user. 260 | let column = model.table.columns[parts[0] + "_id"]! 261 | let table = column.foreignKey!.relatedColumn.table! 262 | return table.columns[parts[1]]! 263 | } 264 | } 265 | 266 | return cols 267 | } 268 | } --------------------------------------------------------------------------------