diff --git a/Library/ClassHandler/Map/SundeedQLiteMap+Normal.swift b/Library/ClassHandler/Map/SundeedQLiteMap+Normal.swift index 3964eb6..a82a908 100644 --- a/Library/ClassHandler/Map/SundeedQLiteMap+Normal.swift +++ b/Library/ClassHandler/Map/SundeedQLiteMap+Normal.swift @@ -411,6 +411,42 @@ public func <~> (left: inout [Float?]?, right: SundeedQLiteMap) { right.addColumn(attribute: left, withColumnName: right.key!) } } +public func <~> (left: inout [Data], right: SundeedQLiteMap) { + if !right.fetchingColumns { + if let value = right.currentValue as? [Data] { + left = value.compactMap({$0}) + } + } else { + right.addColumn(attribute: left, withColumnName: right.key!) + } +} +public func <~> (left: inout [Data?], right: SundeedQLiteMap) { + if !right.fetchingColumns { + if let value = right.currentValue as? [Data] { + left = value.map({$0}) + } + } else { + right.addColumn(attribute: left, withColumnName: right.key!) + } +} +public func <~> (left: inout [Data]?, right: SundeedQLiteMap) { + if !right.fetchingColumns { + if let value = right.currentValue as? [Data] { + left = value.compactMap({$0}) + } + } else { + right.addColumn(attribute: left, withColumnName: right.key!) + } +} +public func <~> (left: inout [Data?]?, right: SundeedQLiteMap) { + if !right.fetchingColumns { + if let value = right.currentValue as? [Data] { + left = value.map({$0}) + } + } else { + right.addColumn(attribute: left, withColumnName: right.key!) + } +} public func <~> (left: inout [UIImage], right: SundeedQLiteMap) { if !right.fetchingColumns { if let value = right.currentValue as? [String] { @@ -626,6 +662,24 @@ public func <~> (left: inout Float, right: SundeedQLiteMap) { right.addColumn(attribute: left, withColumnName: right.key!) } } +public func <~> (left: inout Data?, right: SundeedQLiteMap) { + if !right.fetchingColumns { + if let rightValue = right.currentValue as? Data { + left = rightValue + } + } else { + right.addColumn(attribute: left, withColumnName: right.key!) + } +} +public func <~> (left: inout Data, right: SundeedQLiteMap) { + if !right.fetchingColumns { + if let value = right.currentValue as? Data { + left = value + } + } else { + right.addColumn(attribute: left, withColumnName: right.key!) + } +} public func <~> (left: inout UIImage?, right: SundeedQLiteMap) { if !right.fetchingColumns { if let rightValue = right.currentValue as? String { diff --git a/Library/Main/Processor/Processors/CreateTableProcessor.swift b/Library/Main/Processor/Processors/CreateTableProcessor.swift index 6cb55f9..0e14c18 100644 --- a/Library/Main/Processor/Processors/CreateTableProcessor.swift +++ b/Library/Main/Processor/Processors/CreateTableProcessor.swift @@ -24,11 +24,15 @@ class CreateTableProcessor { if let firstAttribute = attribute.first { try createTableIfNeeded(for: firstAttribute) } - } else - if attribute is [Any] { + } else if attribute is [Any] { createTableForPrimitiveDataTypes(withTableName: columnName) } - createTableStatement.addColumn(with: columnName) + + if attribute is Data { + createTableStatement.addColumn(with: columnName, type: .blob) + } else { + createTableStatement.addColumn(with: columnName, type: .text) + } if columnName == "index" { throw SundeedQLiteError.cantUseNameIndex(tableName: object.tableName) } @@ -36,8 +40,9 @@ class CreateTableProcessor { if objects[Sundeed.shared.primaryKey] != nil { createTableStatement.withPrimaryKey() } - let query: String? = createTableStatement.build() - SundeedQLiteConnection.pool.execute(query: query) + let statement: String? = createTableStatement.build() + SundeedQLiteConnection.pool.execute(query: statement, + parameters: nil) Sundeed.shared.tables.append(object.tableName) } } @@ -46,9 +51,9 @@ class CreateTableProcessor { if !Sundeed.shared.tables.contains(tableName) { let createTableStatement = StatementBuilder() .createTableStatement(tableName: tableName) - .addColumn(with: Sundeed.shared.valueColumnName) + .addColumn(with: Sundeed.shared.valueColumnName, type: .text) .build() - SundeedQLiteConnection.pool.execute(query: createTableStatement) + SundeedQLiteConnection.pool.execute(query: createTableStatement, parameters: nil) Sundeed.shared.tables.append(tableName) } } diff --git a/Library/Main/Processor/Processors/RetrieveProcessor.swift b/Library/Main/Processor/Processors/RetrieveProcessor.swift index 19f1cd0..2fabb90 100644 --- a/Library/Main/Processor/Processors/RetrieveProcessor.swift +++ b/Library/Main/Processor/Processors/RetrieveProcessor.swift @@ -42,17 +42,22 @@ class RetrieveProcessor { } func fetchStatementResult(statement: OpaquePointer?, - columns: [Int : String], + columns: [(columnName: String, columnType: ParameterType)], objectWrapper: ObjectWrapper, subObjectHandler: (_ objectType: String) -> ObjectWrapper?) -> [[String: Any]] { var array: [[String: Any]] = [] while sqlite3_step(statement) == SQLITE_ROW { var dictionary: [String: Any] = [:] var primaryValue: String? - for column in columns { - if let databaseValue = sqlite3_column_text(statement, Int32(column.key)) { + for (index, column) in columns.enumerated() { + if case .blob = column.columnType, + let databaseValue = sqlite3_column_blob(statement, Int32(index)) { + let size = Int(sqlite3_column_bytes(statement, Int32(index))) + let value: Data = Data(bytes: databaseValue, count: size) + dictionary[column.columnName] = value + } else if let databaseValue = sqlite3_column_text(statement, Int32(index)) { let value: String = normalizeColumnValue(databaseValue) - let columnName = column.value + let columnName = column.columnName if value != Sundeed.shared.databaseNull { dictionary[columnName] = value } @@ -119,9 +124,9 @@ class RetrieveProcessor { if sqlite3_prepare_v2(database, selectStatement, -1, &statement, nil) == SQLITE_OK { let columns = getDatabaseColumns(forTable: table) var array: [String] = [] - for column in columns where column.value == Sundeed.shared.valueColumnName { + for (index, column) in columns.enumerated() where column.columnName == Sundeed.shared.valueColumnName { while sqlite3_step(statement) == SQLITE_ROW { - if let columnValue = sqlite3_column_text(statement, Int32(column.key)) { + if let columnValue = sqlite3_column_text(statement, Int32(index)) { let value: String = String(cString: columnValue) if value != Sundeed.shared.databaseNull { array.append(value.replacingOccurrences(of: "\\\"", with: "\"")) @@ -135,23 +140,23 @@ class RetrieveProcessor { SundeedQLiteConnection.pool.closeConnection(database: database) return nil } - func getDatabaseColumns(forTable table: String) -> [Int: String] { + func getDatabaseColumns(forTable table: String) -> [(columnName: String, columnType: ParameterType)] { let database = try? SundeedQLiteConnection.pool.getConnection(toWrite: false) var columnsStatement: OpaquePointer? - var array: [String] = [] - var dictionary: [Int: String] = [:] + var array: [(columnName: String, columnType: ParameterType)] = [] sqlite3_prepare_v2(database, "PRAGMA table_info(\(table));",-1, &columnsStatement, nil) while sqlite3_step(columnsStatement) == SQLITE_ROW { - if let columnName = sqlite3_column_text(columnsStatement, 1) { - array.append(String(cString: columnName)) + if let columnName = sqlite3_column_text(columnsStatement, 1), + let columnType = sqlite3_column_text(columnsStatement, 2) { + array.append((columnName: String(cString: columnName), + columnType: ParameterType(typeString: String(cString: columnType)))) } } - array.enumerated().forEach({dictionary[$0] = $1}) columnsStatement = nil SundeedQLiteConnection.pool.closeConnection(database: database) - return dictionary + return array } func normalizeColumnValue(_ columnValue: UnsafePointer) -> String { String(cString: columnValue).replacingOccurrences(of: "\\\"", with: "\"") diff --git a/Library/Main/Processor/Processors/SaveProcessor.swift b/Library/Main/Processor/Processors/SaveProcessor.swift index 11df63e..8a18388 100644 --- a/Library/Main/Processor/Processors/SaveProcessor.swift +++ b/Library/Main/Processor/Processors/SaveProcessor.swift @@ -113,6 +113,14 @@ class SaveProcessor { completion?() throw SundeedQLiteError.primaryKeyError(tableName: object.tableName) } + } else if let attribute = attribute as? Data { + if objects[Sundeed.shared.primaryKey] as? String != nil { + insertStatement.add(key: columnName, + value: attribute) + } else { + completion?() + throw SundeedQLiteError.primaryKeyError(tableName: object.tableName) + } } else if let attribute = attribute as? [UIImage?] { let compactAttribute = attribute.compactMap({$0}) if compactAttribute.count > 0, @@ -162,8 +170,9 @@ class SaveProcessor { insertStatement.add(key: columnName, value: String(describing: attribute)) } } - let query = insertStatement.build() - SundeedQLiteConnection.pool.execute(query: query, completion: + let statement = insertStatement.build() + SundeedQLiteConnection.pool.execute(query: statement?.query, + parameters: statement?.parameters, completion: { depth = self.completionIfNeeded(depth: depth, completion: completion) }) @@ -189,7 +198,8 @@ class SaveProcessor { .add(key: Sundeed.shared.foreignKey, value: foreignKey) .add(key: Sundeed.shared.valueColumnName, value: String(describing: string)) .build() - SundeedQLiteConnection.pool.execute(query: insertStatement, completion: { + SundeedQLiteConnection.pool.execute(query: insertStatement?.query, + parameters: insertStatement?.parameters, completion: { depth = self.completionIfNeeded(depth: depth) { completion?() } diff --git a/Library/Main/Processor/Processors/UpdateProcessor.swift b/Library/Main/Processor/Processors/UpdateProcessor.swift index b88d362..94b9191 100644 --- a/Library/Main/Processor/Processors/UpdateProcessor.swift +++ b/Library/Main/Processor/Processors/UpdateProcessor.swift @@ -123,8 +123,9 @@ class UpdateProcessor { } } updateStatement.withFilters(filters) - let query: String? = updateStatement.build() - SundeedQLiteConnection.pool.execute(query: query, completion: + let statement = updateStatement.build() + SundeedQLiteConnection.pool.execute(query: statement?.query, + parameters: statement?.parameters, completion: { depth = self.completionIfNeeded(depth: depth, completion: completion) }) diff --git a/Library/Main/Statements/CreateTableStatement.swift b/Library/Main/Statements/CreateTableStatement.swift index 6224742..048ca8d 100644 --- a/Library/Main/Statements/CreateTableStatement.swift +++ b/Library/Main/Statements/CreateTableStatement.swift @@ -9,15 +9,19 @@ import Foundation class CreateTableStatement { + enum ColumnType: String { + case text = "TEXT" + case blob = "BLOB" + } private var tableName: String private var hasPrimaryKey: Bool = false - private var columnNames: [String] = [] + private var columns: [(name: String, type: String)] = [] init(with tableName: String) { self.tableName = tableName } @discardableResult - func addColumn(with columnName: String) -> Self { - columnNames.append(columnName) + func addColumn(with columnName: String, type: ColumnType) -> Self { + columns.append((name: columnName, type: type.rawValue)) return self } @discardableResult @@ -27,8 +31,8 @@ class CreateTableStatement { } func build() -> String? { var statement = "CREATE TABLE IF NOT EXISTS \(tableName) (\(Sundeed.shared.offlineID) INTEGER PRIMARY KEY, \(Sundeed.shared.foreignKey) TEXT, \(Sundeed.shared.fieldNameLink) TEXT" - for columnName in columnNames { - statement.append(",\(columnName) TEXT") + for column in columns { + statement.append(",\(column.name) \(column.type)") } if hasPrimaryKey { statement.append(",CONSTRAINT unq\(tableName) UNIQUE (\(Sundeed.shared.foreignKey),\(Sundeed.shared.primaryKey),\(Sundeed.shared.fieldNameLink))") diff --git a/Library/Main/Statements/InsertStatement.swift b/Library/Main/Statements/InsertStatement.swift index 6427c16..cb895d5 100644 --- a/Library/Main/Statements/InsertStatement.swift +++ b/Library/Main/Statements/InsertStatement.swift @@ -10,28 +10,29 @@ import Foundation class InsertStatement: Statement { private var tableName: String - private var keyValues: [(String, String?)] = [] + private var keyValues: [(String, Any?)] = [] + private var values: [ParameterType] = [] init(with tableName: String) { self.tableName = tableName } @discardableResult - func add(key: String, value: String?) -> Self { + func add(key: String, value: Any?) -> Self { keyValues.append((key, value)) return self } - func build() -> String? { + func build() -> (query: String, parameters: [ParameterType])? { guard !keyValues.isEmpty else { return nil } var statement: String = "REPLACE INTO \(tableName) (" addKeysAndValues(toStatement: &statement) - return statement + return (query: statement, parameters: values) } private func addKeysAndValues(toStatement statement: inout String) { var valuesStatement: String = ") VALUES (" for (index, (key, value)) in keyValues.enumerated() { let value = value ?? "" - let quotation = getQuotation(forValue: value) statement.append(key) - valuesStatement.append("\(quotation)\(value)\(quotation)") + valuesStatement.append("?") + values.append(getParameter(value)) let needed = isLastIndex(index: index, in: keyValues) addSeparatorIfNeeded(separator: ", ", forStatement: &statement, diff --git a/Library/Main/Statements/Statement.swift b/Library/Main/Statements/Statement.swift index df17bac..e10a4a2 100644 --- a/Library/Main/Statements/Statement.swift +++ b/Library/Main/Statements/Statement.swift @@ -21,4 +21,11 @@ class Statement { final func isLastIndex(index: Int, in array: [T]) -> Bool { index != array.count - 1 } + final func getParameter(_ value: Any) -> ParameterType { + if let value = value as? Data { + return .blob(value) + } else { + return .text("\(value)") + } + } } diff --git a/Library/Main/Statements/UpdateStatement.swift b/Library/Main/Statements/UpdateStatement.swift index f547ebd..5368684 100644 --- a/Library/Main/Statements/UpdateStatement.swift +++ b/Library/Main/Statements/UpdateStatement.swift @@ -10,13 +10,14 @@ import Foundation class UpdateStatement: Statement { private var tableName: String - private var keyValues: [(String, String)] = [] + private var keyValues: [(String, Any?)] = [] + private var values: [ParameterType] = [] private var filters: [SundeedExpression] = [] init(with tableName: String) { self.tableName = tableName } @discardableResult - func add(key: String, value: String) -> Self { + func add(key: String, value: Any?) -> Self { keyValues.append((key, value)) return self } @@ -25,18 +26,18 @@ class UpdateStatement: Statement { self.filters = filters.compactMap({$0}) return self } - func build() -> String? { + func build() -> (query: String, parameters: [ParameterType])? { guard !keyValues.isEmpty else { return nil } var statement = "UPDATE \(tableName) SET " addKeyValues(toStatement: &statement) addFilters(toStatement: &statement) - return statement + return (query: statement, parameters: values) } private func addKeyValues(toStatement statement: inout String) { for (index, (key, value)) in keyValues.enumerated() { - let value = value - let quotation = getQuotation(forValue: value) - statement.append("\(key) = \(quotation)\(value)\(quotation)") + let value = value ?? "" + statement.append("\(key) = ?") + values.append(getParameter(value)) addSeparatorIfNeeded(separator: ", ", forStatement: &statement, needed: isLastIndex(index: index, in: keyValues)) diff --git a/Library/Main/SundeedQLiteConnection.swift b/Library/Main/SundeedQLiteConnection.swift index bed19e5..278bf9e 100644 --- a/Library/Main/SundeedQLiteConnection.swift +++ b/Library/Main/SundeedQLiteConnection.swift @@ -9,14 +9,29 @@ import Foundation import SQLite3 +enum ParameterType { + case text(String) + case blob(Data) + + init(typeString: String) { + switch typeString { + case "BLOB": + self = .blob(Data()) + default: + self = .text("") + } + } +} class SundeedQLiteConnection { static var pool: SundeedQLiteConnection = SundeedQLiteConnection() - var sqlStatements: [(String, (()->Void)?)] = [] + var sqlStatements: [(String, [ParameterType]?, (()->Void)?)] = [] var canExecute: Bool = true let fileManager = FileManager.default let destPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + lazy var fullDestPath = NSURL(fileURLWithPath: destPath) .appendingPathComponent(Sundeed.shared.databaseFileName) func getConnection(toWrite: Bool = false) throws -> OpaquePointer? { @@ -48,7 +63,7 @@ class SundeedQLiteConnection { closeConnection(database: database) } } - func execute(query: String?, force: Bool = false, completion: (()->Void)? = nil) { + func execute(query: String?, parameters: [ParameterType]? = nil, force: Bool = false, completion: (()->Void)? = nil) { guard let query = query else { completion?() return @@ -61,16 +76,26 @@ class SundeedQLiteConnection { var statement: OpaquePointer? let prepare = sqlite3_prepare_v2(writeConnection, query, -1, &statement, nil) if prepare == SQLITE_OK { + parameters?.enumerated().forEach({ (index, value) in + switch value { + case .text(let value): + sqlite3_bind_text(statement, Int32(index+1), + value, -1, self.SQLITE_TRANSIENT) + case .blob(let data): + sqlite3_bind_blob(statement, Int32(index+1), + NSData(data: data).bytes, Int32(NSData(data: data).length), self.SQLITE_TRANSIENT) + } + }) if sqlite3_step(statement) == SQLITE_DONE { sqlite3_finalize(statement) } else { sqlite3_finalize(statement) - self.sqlStatements.insert((query, completion), at: 0) + self.sqlStatements.insert((query, parameters, completion), at: 0) } } else { if prepare != SQLITE_ERROR { sqlite3_finalize(statement) - self.sqlStatements.insert((query, completion), at: 0) + self.sqlStatements.insert((query, parameters, completion), at: 0) } } let combination = self.sqlStatements.popLast() @@ -78,7 +103,7 @@ class SundeedQLiteConnection { self.closeConnection(database: writeConnection) statement = nil writeConnection = nil - self.execute(query: oldQuery, force: true, completion: combination?.1) + self.execute(query: oldQuery, parameters: combination?.1, force: true, completion: combination?.2) } else { completion?() self.closeConnection(database: writeConnection) @@ -87,10 +112,10 @@ class SundeedQLiteConnection { self.canExecute = true } } else { - self.sqlStatements.insert((query, completion), at: 0) + self.sqlStatements.insert((query, parameters, completion), at: 0) } } catch { - self.sqlStatements.insert((query, completion), at: 0) + self.sqlStatements.insert((query, parameters, completion), at: 0) } } } diff --git a/SundeedQLiteLibraryTests/ClassesForTesting.swift b/SundeedQLiteLibraryTests/ClassesForTesting.swift index 12d51e3..1099e7f 100644 --- a/SundeedQLiteLibraryTests/ClassesForTesting.swift +++ b/SundeedQLiteLibraryTests/ClassesForTesting.swift @@ -59,6 +59,9 @@ enum Type { class EmployerForTesting: SundeedQLiter { var type: Type? var mandatoryType: Type = .manager + var data: Data? + var mandatoryData: Data = "Test Data".data(using: .utf8) ?? Data() + var optionalData: Data? var arrayOfTypes: [Type] = [] var optionalArrayOfTypes: [Type]? var optionalArrayOfOptionalTypes: [Type?]? @@ -152,6 +155,9 @@ class EmployerForTesting: SundeedQLiter { func sundeedQLiterMapping(map: SundeedQLiteMap) { type <~> (map["type"], TypeConverter()) mandatoryType <~> (map["mandatoryType"], TypeConverter()) + data <~> map["data"] + mandatoryData <~> map["mandatoryData"] + optionalData <~> map["optionalData"] arrayOfTypes <~> (map["arrayOfTypes"], TypeConverter()) optionalArrayOfTypes <~> (map["optionalArrayOfTypes"], TypeConverter()) optionalArrayOfOptionalTypes <~> (map["optionalArrayOfOptionalTypes"], TypeConverter()) @@ -254,6 +260,9 @@ class EmployerForTesting: SundeedQLiter { } type = .manager mandatoryType = .ceo + data = "Testing Data".data(using: .utf8) + mandatoryData = "Testing Mandatory Data".data(using: .utf8) ?? Data() + optionalData = "Testing Optional Data".data(using: .utf8) arrayOfTypes = [.manager, .ceo] optionalArrayOfTypes = [.manager, .ceo] optionalArrayOfOptionalTypes = [.manager, .ceo] @@ -318,6 +327,9 @@ class EmployerForTesting: SundeedQLiter { class EmployerWithNoPrimaryForTesting: SundeedQLiter { var type: Type? + var data: Data? + var mandatoryData: Data = "Test Data".data(using: .utf8) ?? Data() + var optionalData: Data? var string: String = "" var optionalString: String? var object: EmployeeForTesting = EmployeeForTesting(id: "EFGH-9012-IJKL-3456") @@ -406,6 +418,9 @@ class EmployerWithNoPrimaryForTesting: SundeedQLiter { required init() {} func sundeedQLiterMapping(map: SundeedQLiteMap) { type <~> (map["type"], TypeConverter()) + data <~> map["data"] + mandatoryData <~> map["mandatoryData"] + optionalData <~> map["optionalData"] string <~> map["string"] optionalString <~> map["optionalString"] object <~> map["object"] @@ -503,6 +518,9 @@ class EmployerWithNoPrimaryForTesting: SundeedQLiter { employees.append(employee) } type = .manager + data = "Testing Data".data(using: .utf8) + mandatoryData = "Testing Mandatory Data".data(using: .utf8) ?? Data() + optionalData = "Testing Optional Data".data(using: .utf8) string = "string" optionalString = "optionalString" object = employees[0] diff --git a/SundeedQLiteLibraryTests/Main/SundeedQLiteLibraryTests.swift b/SundeedQLiteLibraryTests/Main/SundeedQLiteLibraryTests.swift index b05f81f..fb993d5 100644 --- a/SundeedQLiteLibraryTests/Main/SundeedQLiteLibraryTests.swift +++ b/SundeedQLiteLibraryTests/Main/SundeedQLiteLibraryTests.swift @@ -96,7 +96,32 @@ class SundeedQLiteLibraryTests: XCTestCase { let query = UpdateStatement(with: "table") .add(key: "column1", value: "value1") .build() - XCTAssertEqual(query, "UPDATE table SET column1 = \'value1\' WHERE 1") + XCTAssertEqual(query?.query, "UPDATE table SET column1 = ? WHERE 1") + XCTAssertEqual(query?.parameters.count, 1) + switch query?.parameters.first { + case .text(let text): + XCTAssertEqual(text, "value1") + case .blob: + XCTFail("UPDATE IS NOT BLOB") + case .none: + XCTFail("PARAMETERS SHOULDN'T BE NIL") + } + } + + func testUpdateDataWithNoFilter() { + let query = UpdateStatement(with: "table") + .add(key: "column1", value: "value1".data(using: .utf8)) + .build() + XCTAssertEqual(query?.query, "UPDATE table SET column1 = ? WHERE 1") + XCTAssertEqual(query?.parameters.count, 1) + switch query?.parameters.first { + case .text: + XCTFail("UPDATE IS NOT TEXT") + case .blob(let data): + XCTAssertEqual(String(data: data, encoding: .utf8), "value1") + case .none: + XCTFail("PARAMETERS SHOULDN'T BE NIL") + } } func testDeleteWithNoFilter() { diff --git a/SundeedQLiteLibraryTests/Operations/OperationTests.swift b/SundeedQLiteLibraryTests/Operations/OperationTests.swift index f47887f..80e3d58 100644 --- a/SundeedQLiteLibraryTests/Operations/OperationTests.swift +++ b/SundeedQLiteLibraryTests/Operations/OperationTests.swift @@ -556,6 +556,9 @@ class OperationTests: XCTestCase { private func checkEmployer(_ employer: EmployerForTesting) { XCTAssertEqual(employer.type, .manager) + XCTAssertEqual(employer.data, "Testing Data".data(using: .utf8)) + XCTAssertEqual(employer.mandatoryData, "Testing Mandatory Data".data(using: .utf8) ?? Data()) + XCTAssertEqual(employer.optionalData, "Testing Optional Data".data(using: .utf8)) XCTAssertEqual(employer.mandatoryType, .ceo) XCTAssertEqual(employer.arrayOfTypes, [.manager, .ceo]) XCTAssertEqual(employer.optionalArrayOfTypes, [.manager, .ceo])