Skip to content

Instantly share code, notes, and snippets.

@Ilesh
Created October 20, 2020 17:07
Show Gist options
  • Save Ilesh/38b767cd43fbddcf01f58e7385fd8fcd to your computer and use it in GitHub Desktop.
Save Ilesh/38b767cd43fbddcf01f58e7385fd8fcd to your computer and use it in GitHub Desktop.
SQLiteDB files for easy to use...
//
// CloudDB.swift
// SQLiteDB-iOS
//
// Created by Ilesh.
// Copyright © 2019. All rights reserved.
//
import CloudKit
enum DBType: Int {
case none, publicDB, privateDB, sharedDB
}
// MARK:- CloudDB Class
/// Class for remotely saving local SQLiteDB data using CloudKit
@objc(CloudDB)
class CloudDB: NSObject {
/// Singleton instance for access to the CloudDB class
static let shared = CloudDB()
/// Default CloudKit container
private let container = CKContainer.default()
/// Reference to public CloudKit database
private let publicDB: CKDatabase
/// Reference to private CloudKit database
private let privateDB: CKDatabase
/// Reference to shared CloudKit database
private let sharedDB: CKDatabase!
override private init() {
self.publicDB = container.publicCloudDatabase
self.privateDB = container.privateCloudDatabase
if #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) {
self.sharedDB = container.sharedCloudDatabase
} else {
self.sharedDB = nil
}
super.init()
}
/// Create a record zone in the private DB for the given table
/// - Parameter version: An integer value indicating the new DB version.
func creaeZone(table: SQLTable, completion: @escaping ()->Void) {
let zone = CKRecordZone(zoneName: table.table)
privateDB.save(zone) {(_, error) in
if let error = error {
NSLog("Error creating record zone for: \(table.table) - \(error.localizedDescription)")
}
completion()
}
}
func getUpdates(table: SQLTable) {
if table.remoteDB() == DBType.privateDB {
// Get updates via CKFetchRecordChangesOperation
} else {
// Get all updates via CKFetchDatabaseChangesOperation
}
}
/// Save data to the cloud via CloudKit
/// - Parameters:
/// - row: The SQLTable instance to be saved remotely.
/// - dbOverride: A `DBType` indicating the database to save the remote data to. If set, this overrides the database set by default for the table via the `remoteDB` method. Defaults to `none`.
func saveToCloud(row: SQLTable, dbOverride: DBType = .none) {
var type = row.remoteDB()
if dbOverride != .none {
type = dbOverride
}
// Set up remote ID
let idName = row.remoteKey()
var sid = ""
let rid = recordIDFor(row: row, type: type)
if let rid = rid {
sid = rid.recordName
}
// Create CloudKit record
let record = recordFor(recordID: rid, row: row, type: type)
// Save to DB
let db = dbFor(type: type)
db.save(record) {(rec, error) in
if let error = error {
NSLog("Error saving CloudKit data: \(error.localizedDescription)")
return
}
// Save remote id locally
if let rec = rec {
let ckid = rec.recordID.recordName
if sid != ckid {
row.setValue(ckid, forKey: idName)
_ = row.save(updateCloud: false)
}
NSLog("Saved record successfully! ID - \(ckid)")
}
}
}
/// Delete data from the cloud via CloudKit
/// - Parameters:
/// - row: The SQLTable instance to be deleted remotely.
/// - dbOverride: A `DBType` indicating the database to delete the remote data from. If set, this overrides the database set by default for the table via the `remoteDB` method. Defaults to `none`.
func deleteFromCloud(row: SQLTable, dbOverride: DBType = .none) {
var type = row.remoteDB()
if dbOverride != .none {
type = dbOverride
}
// DB to use
let db = dbFor(type: type)
// Set up remote ID
guard let ckid = recordIDFor(row: row, type: type) else { return }
db.delete(withRecordID: ckid) { (rid, error) in
if let error = error {
NSLog("Error deleting CloudKit record: \(error.localizedDescription)")
return
}
NSLog("Deleted record successfully! ID - \(rid!.recordName)")
}
}
/// Fetch changes for a given SQLTable sub-class and update the table with the changes. This can only be run on the private CloudKit database - so assumes that the call is for the private DB.
/// - Parameter row: A instance from an `SQLTable` sub-class. We need this to get relevant row information. So if necessary, just pass a newly created instance - the passed in row is not modified in any way.
func fetchChanges(row:SQLTable) {
}
// MARK:- Private Methods
/// Get the CloudKit database to use dependent on the passed-in database type
/// - Parameter type: The database type - should be one of `.public`, `.private`, or `.shared`.
private func dbFor(type: DBType) -> CKDatabase {
// DB to use
switch type {
case .publicDB:
return publicDB
case .privateDB:
return privateDB
case .sharedDB:
return sharedDB
case .none:
assertionFailure("Should not have received a DBType of .none to get DB!")
}
return publicDB
}
/// Get the CloudKit record ID for the passed in SQLTable sub-class. The method creates a record ID if there's a valid record ID. If not, it returns `nil`.
/// - row: The SQLTable instance to be deleted remotely.
/// - type: The database type - should be one of `.public`, `.private`, or `.shared`.
private func recordIDFor(row: SQLTable, type: DBType) -> CKRecord.ID? {
let data = row.values()
// Set up remote ID
let idName = row.remoteKey()
if let sid = data[idName] as? String, !sid.isEmpty {
if type == .privateDB {
let zone = CKRecordZone.ID(zoneName: row.table, ownerName: CKCurrentUserDefaultName)
return CKRecord.ID(recordName: sid, zoneID: zone)
} else {
return CKRecord.ID(recordName: sid)
}
}
return nil
}
/// Get the CloudKit record for the passed in SQLTable sub-class. The method creates a new CKRecord instance containing the data from the `SQLTable` sub-class.
/// - row: The SQLTable instance to be deleted remotely.
/// - type: The database type - should be one of `.public`, `.private`, or `.shared`.
private func recordFor(recordID: CKRecord.ID?, row: SQLTable, type: DBType) -> CKRecord {
let data = row.values()
let idName = row.remoteKey()
let record: CKRecord
if let ckid = recordID {
record = CKRecord(recordType: row.table, recordID: ckid)
} else {
if type == .privateDB {
let zone = CKRecordZone.ID(zoneName: row.table, ownerName: CKCurrentUserDefaultName)
record = CKRecord(recordType: row.table, zoneID: zone)
} else {
record = CKRecord(recordType: row.table)
}
}
for (key, val) in data {
if let ckval = val as? CKRecordValue {
// Handle CloudKit ID
if key == idName {
continue
}
record[key] = ckval
}
}
return record
}
}
//
// SQLiteBase.swift
// SQLiteDB
//
// Created by Ilesh.
// Copyright © 2019. All rights reserved.
//
import Foundation
let SQLITE_DATE = SQLITE_NULL + 1
private let SQLITE_STATIC = unsafeBitCast(0, to:sqlite3_destructor_type.self)
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to:sqlite3_destructor_type.self)
// MARK:- SQLiteBase Class
/// Simple wrapper class to provide basic SQLite database access as a non-singleton
@objc(SQLiteBase)
class SQLiteBase: NSObject {
/// Internal name for GCD queue used to execute SQL commands so that all commands are executed sequentially
private let QUEUE_LABEL = "SQLiteDB"
/// The internal GCD queue
private var queue:DispatchQueue!
/// Internal handle to the currently open SQLite DB instance
internal var db:OpaquePointer? = nil
/// Internal DateFormatter instance used to manage date formatting
private let fmt = DateFormatter()
private let fmtAPI = DateFormatter()
private let fmtAPIFull = DateFormatter()
/// Internal reference to the currently open database path
internal var path:String!
override init() {
super.init()//Owner Phone
// Set up essentials
queue = DispatchQueue(label:QUEUE_LABEL, attributes:[])
// You need to set the locale in order for the 24-hour date format to work correctly on devices where 24-hour format is turned off
fmt.locale = Locale(identifier:"en_US_POSIX")
fmt.timeZone = TimeZone(secondsFromGMT:0)
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss" //
fmtAPI.locale = Locale(identifier:"en_US_POSIX")
fmtAPI.timeZone = TimeZone(secondsFromGMT:0)
fmtAPI.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"//"yyyy-MM-dd'T'HH:mm:ssZZZZZ" //2019-07-10T00:00:00+00:00
fmtAPIFull.locale = Locale(identifier:"en_US_POSIX")
fmtAPIFull.timeZone = TimeZone(secondsFromGMT:0)
fmtAPIFull.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SS"//"2019-10-04T05:47:17.69"
}
deinit {
closeDB()
}
/// Output the current SQLite database path
override var description:String {
return "SQLiteBase: \(String(describing: path))"
}
// MARK:- Public Methods
/// Open the database specified by the `DB_NAME` variable and assigns the internal DB references. If a database is currently open, the method first closes the current database and gets a new DB references to the current database pointed to by `DB_NAME`
///
/// - Parameter copyFile: Whether to copy the file named in `DB_NAME` from resources or to create a new empty database file. Defaults to `true`
/// - Returns: Returns a boolean value indicating if the database was successfully opened or not.
func open(dbPath: String, copyFile:Bool = false) -> Bool {
if db != nil {
closeDB()
}
let url = URL(fileURLWithPath: dbPath)
// Set up for file operations
let fm = FileManager.default
var dest = url
// Set up destination path to DB in Documents directory - if copying
if copyFile {
guard var docDir = fm.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first else { return false }
// If macOS, add app name to path since otherwise, DB could possibly interfere with another app using SQLiteDB
#if os(OSX)
let info = Bundle.main.infoDictionary!
let appName = info["CFBundleName"] as! String
docDir = docDir.appendingPathComponent(appName)
// Create folder if it does not exist
if !fm.fileExists(atPath: docDir.path) {
do {
try fm.createDirectory(at: docDir, withIntermediateDirectories: true, attributes: nil)
} catch {
assert(false, "SQLiteDB: Error creating DB directory: \(docDir) on macOS")
return false
}
}
#endif
let fn = url.lastPathComponent
dest = docDir.appendingPathComponent(fn)
// Check if DB is there in Documents directory
if !(fm.fileExists(atPath: dest.path)) && copyFile {
// The database does not exist, so copy it
do {
try fm.copyItem(at: url, to: dest)
} catch let error {
assert(false, "SQLiteDB: Failed to copy writable version of DB! Error - \(error.localizedDescription)")
return false
}
}
}
// Open the DB
path = dest.path
let cpath = path.cString(using:String.Encoding.utf8)
let error = sqlite3_open(cpath!, &db)
if error != SQLITE_OK {
// Open failed, close DB and fail
NSLog("SQLiteDB - failed to open DB!")
sqlite3_close(db)
return false
}
NSLog("SQLiteDB opened! \n PATH:\(String(describing: path))")
return true
}
/// Close the currently open SQLite database.
func closeDB() {
if db != nil {
sqlite3_close(db)
self.db = nil
}
}
/// Returns an ISO-8601 date string for a given date.
///
/// - Parameter date: The date to format in to an ISO-8601 string
/// - Returns: A string with the date in ISO-8601 format.
func dbDate(date:Date) -> String {
return fmt.string(from:date)
}
/// Execute SQL (non-query) command with (optional) parameters and return result code
///
/// - Parameters:
/// - sql: The SQL statement to be executed
/// - parameters: An array of optional parameters in case the SQL statement includes bound parameters - indicated by `?`
/// - Returns: The ID for the last inserted row (if it was an INSERT command and the ID is an integer column) or a result code indicating the status of the command execution. A non-zero result indicates success and a 0 indicates failure.
func execute(sql:String, parameters:[Any]? = nil)->Int {
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
var result = 0
queue.sync {
if let stmt = self.prepare(sql:sql, params:parameters) {
result = self.execute(stmt:stmt, sql:sql)
}
}
return result
}
/// Run an SQL query with (parameters) parameters and returns an array of dictionaries where the keys are the column names
///
/// - Parameters:
/// - sql: The SQL query to be executed
/// - parameters: An array of optional parameters in case the SQL statement includes bound parameters - indicated by `?`
/// - Returns: An empty array if the query resulted in no rows. Otherwise, an array of dictionaries where each dictioanry key is a column name and the value is the column value.
func query(sql:String, parameters:[Any]? = nil)->[[String:Any]] {
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
var rows = [[String:Any]]()
queue.sync {
if let stmt = self.prepare(sql:sql, params:parameters) {
rows = self.query(stmt:stmt, sql:sql)
}
}
return rows
}
/// Get the current internal DB version
///
/// - Returns: An interger indicating the current internal DB version as set by the developer via code. If a DB version was not set, this defaults to 0.
func getDBVersion() -> Int {
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
var version = 0
let arr = query(sql:"PRAGMA user_version")
if arr.count == 1 {
version = arr[0]["user_version"] as! Int
}
return version
}
/// Set the current DB version, a user-defined version number for the database. This value can be useful in managing data migrations so that you can add new columns to your tables or massage your existing data to suit a new situation.
///
/// - Parameter version: An integer value indicating the new DB version.
func set(version:Int) {
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
_ = execute(sql:"PRAGMA user_version=\(version)")
}
// MARK:- Private Methods
/// Private method to prepare an SQL statement before executing it.
///
/// - Parameters:
/// - sql: The SQL query or command to be prepared.
/// - params: An array of optional parameters in case the SQL statement includes bound parameters - indicated by `?`
/// - Returns: A pointer to a finalized SQLite statement that can be used to execute the query later
private func prepare(sql:String, params:[Any]?) -> OpaquePointer? {
var stmt:OpaquePointer? = nil
let cSql = sql.cString(using: String.Encoding.utf8)
// Prepare
let result = sqlite3_prepare_v2(self.db, cSql!, -1, &stmt, nil)
if result != SQLITE_OK {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "SQLiteDB - failed to prepare SQL: \(sql), Error: \(error)"
NSLog(msg)
}
return nil
}
// Bind parameters, if any
if params != nil {
// Validate parameters
let cntParams = sqlite3_bind_parameter_count(stmt)
let cnt = params!.count
if cntParams != CInt(cnt) {
let msg = "SQLiteDB - failed to bind parameters, counts did not match. SQL: \(sql), Parameters: \(params!)"
NSLog(msg)
return nil
}
var flag:CInt = 0
// Text & BLOB values passed to a C-API do not work correctly if they are not marked as transient.
for ndx in 1...cnt {
// NSLog("Binding: \(params![ndx-1]) at Index: \(ndx)")
// Check for data types
if let txt = params![ndx-1] as? String {
flag = sqlite3_bind_text(stmt, CInt(ndx), txt, -1, SQLITE_TRANSIENT)
} else if let data = params![ndx-1] as? NSData {
flag = sqlite3_bind_blob(stmt, CInt(ndx), data.bytes, CInt(data.length), SQLITE_TRANSIENT)
} else if let date = params![ndx-1] as? Date {
let txt = fmt.string(from:date)
flag = sqlite3_bind_text(stmt, CInt(ndx), txt, -1, SQLITE_TRANSIENT)
} else if let val = params![ndx-1] as? Bool {
let num = val ? 1 : 0
flag = sqlite3_bind_int(stmt, CInt(ndx), CInt(num))
} else if let val = params![ndx-1] as? Double {
flag = sqlite3_bind_double(stmt, CInt(ndx), CDouble(val))
} else if let val = params![ndx-1] as? Int {
flag = sqlite3_bind_int(stmt, CInt(ndx), CInt(val))
} else {
flag = sqlite3_bind_null(stmt, CInt(ndx))
}
// Check for errors
if flag != SQLITE_OK {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "SQLiteDB - failed to bind for SQL: \(sql), Parameters: \(params!), Index: \(ndx) Error: \(error)"
NSLog(msg)
}
return nil
}
}
}
return stmt
}
/// Private method which handles the actual execution of an SQL statement which had been prepared previously.
///
/// - Parameters:
/// - stmt: The previously prepared SQLite statement
/// - sql: The SQL command to be excecuted
/// - Returns: The ID for the last inserted row (if it was an INSERT command and the ID is an integer column) or a result code indicating the status of the command execution. A non-zero result indicates success and a 0 indicates failure.
private func execute(stmt:OpaquePointer, sql:String)->Int {
// Step
let res = sqlite3_step(stmt)
if res != SQLITE_OK && res != SQLITE_DONE {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "SQLiteDB - failed to execute SQL: \(sql), Error: \(error)"
NSLog(msg)
}
return 0
}
// Is this an insert
let upp = sql.uppercased()
var result = 0
if upp.hasPrefix("INSERT ") {
// Known limitations: http://www.sqlite.org/c3ref/last_insert_rowid.html
let rid = sqlite3_last_insert_rowid(self.db)
result = Int(rid)
} else if upp.hasPrefix("DELETE") || upp.hasPrefix("UPDATE") {
var cnt = sqlite3_changes(self.db)
if cnt == 0 {
cnt += 1
}
result = Int(cnt)
} else {
result = 1
}
// Finalize
sqlite3_finalize(stmt)
return result
}
/// executeTransaction SQL (non-query) command with (optional) parameters and return result code
///
/// - Parameters:
/// - sql: The SQL statement to be executed
/// - parameters: An array of optional parameters in case the SQL statement includes bound parameters - indicated by `?`
/// - Returns: The ID for the last inserted row (if it was an INSERT command and the ID is an integer column) or a result code indicating the status of the command execution. A non-zero result indicates success and a 0 indicates failure.
func executeTransaction(sql:String, parameters:[[Any]]? = nil)->Int {
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
var result = 0
queue.sync {
result = self.prepareTransaction(sql:sql, arrParams:parameters)
}
return result
}
// MARK:- Private Methods
/// Private method to prepare an SQL statement before executing it.
///
/// - Parameters:
/// - sql: The SQL query or command to be prepared.
/// - params: An array of optional parameters in case the SQL statement includes bound parameters - indicated by `?`
/// - Returns: A pointer to a finalized SQLite statement that can be used to execute the query later
private func prepareTransaction(sql:String, arrParams:[[Any]]?) -> Int {
var stmt:OpaquePointer? = nil
let cSql = sql.cString(using: String.Encoding.utf8)
// Prepare
sqlite3_exec(db,"BEGIN IMMEDIATE TRANSACTION", nil, nil, nil)
var result = sqlite3_prepare_v2(self.db, cSql!, -1, &stmt, nil)
if result != SQLITE_OK {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "SQLiteDB - failed to prepare SQL: \(sql), Error: \(error)"
NSLog(msg)
}
return 0
}
// Bind parameters, if any
if arrParams != nil {
for element in arrParams! {
var flag:CInt = 0
// Text & BLOB values passed to a C-API do not work correctly if they are not marked as transient.
for ndx in 1...element.count {
// NSLog("Binding: \(params![ndx-1]) at Index: \(ndx)")
// Check for data types
if let txt = element[ndx-1] as? String {
flag = sqlite3_bind_text(stmt, CInt(ndx), txt, -1, SQLITE_TRANSIENT)
} else if let data = element[ndx-1] as? NSData {
flag = sqlite3_bind_blob(stmt, CInt(ndx), data.bytes, CInt(data.length), SQLITE_TRANSIENT)
} else if let date = element[ndx-1] as? Date {
let txt = fmt.string(from:date)
flag = sqlite3_bind_text(stmt, CInt(ndx), txt, -1, SQLITE_TRANSIENT)
} else if let val = element[ndx-1] as? Bool {
let num = val ? 1 : 0
flag = sqlite3_bind_int(stmt, CInt(ndx), CInt(num))
}else if let val = element[ndx-1] as? Int {
flag = sqlite3_bind_int64(stmt, Int32(ndx), sqlite3_int64(val))
}
else if let val = element[ndx-1] as? Double {
flag = sqlite3_bind_double(stmt, CInt(ndx), CDouble(val))
}else {
flag = sqlite3_bind_null(stmt, CInt(ndx))
}
}
if sqlite3_step(stmt) == SQLITE_DONE {
// print("Successfully inserted row.")
} else {
print("Could not insert row.")
}
sqlite3_reset(stmt)
// Check for errors
if flag != SQLITE_OK {
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "SQLiteDB - failed to bind for SQL: \(sql), Parameters: \(element), Index: \(0) Error: \(error)"
NSLog(msg)
}
return 0
}
}
sqlite3_finalize(stmt)
if (sqlite3_exec(db,"COMMIT TRANSACTION", nil, nil, nil) !=
SQLITE_OK) {
NSLog("%@",sqlite3_errmsg(db));
result = 0
}else{
result = 1
}
}
return Int(result)
}
private func executeWithTransaction(stmt:OpaquePointer, sql:String)->Int {
// Step
let res = sqlite3_step(stmt)
if res != SQLITE_OK && res != SQLITE_DONE {
sqlite3_finalize(stmt)
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
let msg = "SQLiteDB - failed to execute SQL: \(sql), Error: \(error)"
NSLog(msg)
}
return 0
}
// Is this an insert
let upp = sql.uppercased()
var result = 0
if upp.hasPrefix("INSERT ") {
// Known limitations: http://www.sqlite.org/c3ref/last_insert_rowid.html
let rid = sqlite3_last_insert_rowid(self.db)
result = Int(rid)
} else if upp.hasPrefix("DELETE") || upp.hasPrefix("UPDATE") {
var cnt = sqlite3_changes(self.db)
if cnt == 0 {
cnt += 1
}
result = Int(cnt)
} else {
result = 1
}
// Finalize
sqlite3_finalize(stmt)
return result
}
/// Private method which handles the actual execution of an SQL query which had been prepared previously.
///
/// - Parameters:
/// - stmt: The previously prepared SQLite statement
/// - sql: The SQL query to be run
/// - Returns: An empty array if the query resulted in no rows. Otherwise, an array of dictionaries where each dictioanry key is a column name and the value is the column value.
private func query(stmt:OpaquePointer, sql:String)->[[String:Any]] {
var rows = [[String:Any]]()
var fetchColumnInfo = true
var columnCount:CInt = 0
var columnNames = [String]()
var columnTypes = [CInt]()
var result = sqlite3_step(stmt)
while result == SQLITE_ROW {
// Should we get column info?
if fetchColumnInfo {
columnCount = sqlite3_column_count(stmt)
for index in 0..<columnCount {
// Get column name
let name = sqlite3_column_name(stmt, index)
columnNames.append(String(validatingUTF8:name!)!)
// Get column type
columnTypes.append(self.getColumnType(index:index, stmt:stmt))
}
fetchColumnInfo = false
}
// Get row data for each column
var row = [String:Any]()
for index in 0..<columnCount {
let key = columnNames[Int(index)]
let type = columnTypes[Int(index)]
if let val = getColumnValue(index:index, type:type, stmt:stmt) {
// NSLog("Column type:\(type) with value:\(val)")
row[key] = val
}
}
rows.append(row)
// Next row
result = sqlite3_step(stmt)
}
sqlite3_finalize(stmt)
return rows
}
/// Private method that returns the declared SQLite data type for a specific column in a pre-prepared SQLite statement.
///
/// - Parameters:
/// - index: The 0-based index of the column
/// - stmt: The previously prepared SQLite statement
/// - Returns: A CInt value indicating the SQLite data type
private func getColumnType(index:CInt, stmt:OpaquePointer)->CInt {
var type:CInt = 0
// Column types - http://www.sqlite.org/datatype3.html (section 2.2 table column 1)
let blobTypes = ["BINARY", "BLOB", "VARBINARY"]
let charTypes = ["CHAR", "CHARACTER", "CLOB", "NATIONAL VARYING CHARACTER", "NATIVE CHARACTER", "NCHAR", "NVARCHAR", "TEXT", "VARCHAR", "VARIANT", "VARYING CHARACTER"]
let dateTypes = ["DATE", "DATETIME", "TIME", "TIMESTAMP"]
let intTypes = ["BIGINT", "BIT", "BOOL", "BOOLEAN", "INT", "INT2", "INT8", "INTEGER", "MEDIUMINT", "SMALLINT", "TINYINT"]
let nullTypes = ["NULL"]
let realTypes = ["DECIMAL", "DOUBLE", "DOUBLE PRECISION", "FLOAT", "NUMERIC", "REAL"]
// Determine type of column - http://www.sqlite.org/c3ref/c_blob.html
let buf = sqlite3_column_decltype(stmt, index)
// NSLog("SQLiteDB - Got column type: \(buf)")
if buf != nil {
var tmp = String(validatingUTF8:buf!)!.uppercased()
// Remove bracketed section
if let pos = tmp.range(of:"(") {
tmp = String(tmp[..<pos.lowerBound])
}
// Remove unsigned?
// Remove spaces
// Is the data type in any of the pre-set values?
// NSLog("SQLiteDB - Cleaned up column type: \(tmp)")
if intTypes.contains(tmp) {
return SQLITE_INTEGER
}
if realTypes.contains(tmp) {
return SQLITE_FLOAT
}
if charTypes.contains(tmp) {
return SQLITE_TEXT
}
if blobTypes.contains(tmp) {
return SQLITE_BLOB
}
if nullTypes.contains(tmp) {
return SQLITE_NULL
}
if dateTypes.contains(tmp) {
return SQLITE_DATE
}
return SQLITE_TEXT
} else {
// For expressions and sub-queries
type = sqlite3_column_type(stmt, index)
}
return type
}
// Get column value
/// Private method to return the column value for a specified SQLite column.
///
/// - Parameters:
/// - index: The 0-based index of the column
/// - type: The declared SQLite data type for the column
/// - stmt: The previously prepared SQLite statement
/// - Returns: A value for the column if the data is of a recognized SQLite data type, or nil if the value was NULL
private func getColumnValue(index:CInt, type:CInt, stmt:OpaquePointer)->Any? {
// Integer
if type == SQLITE_INTEGER {
let val = sqlite3_column_int64(stmt, index)
return Int(val)
}
// Float
if type == SQLITE_FLOAT {
let val = sqlite3_column_double(stmt, index)
return Double(val)
}
// Text - handled by default handler at end
// Blob
if type == SQLITE_BLOB {
let data = sqlite3_column_blob(stmt, index)
let size = sqlite3_column_bytes(stmt, index)
let val = NSData(bytes:data, length:Int(size))
return val
}
// Null
if type == SQLITE_NULL {
return nil
}
// Date
if type == SQLITE_DATE {
// Is this a text date
if let ptr = UnsafeRawPointer.init(sqlite3_column_text(stmt, index)) {
let uptr = ptr.bindMemory(to:CChar.self, capacity:0)
if let txt = String(validatingUTF8:uptr) {
// Get date from string
if let dt = fmt.date(from: txt) {
return dt
}else if let dt = fmtAPI.date(from: txt) {
return dt
}else if let dt = fmtAPIFull.date(from: txt) {
return dt
}
else {
NSLog("String value: \(txt) but could not be converted to date!")
}
}
}
// If not a text date, then it's a time interval
let val = sqlite3_column_double(stmt, index)
let dt = Date(timeIntervalSince1970: val)
return dt
}
// If nothing works, return a string representation
if let ptr = UnsafeRawPointer.init(sqlite3_column_text(stmt, index)) {
let uptr = ptr.bindMemory(to:CChar.self, capacity:0)
let txt = String(validatingUTF8:uptr)
return txt
}
return nil
}
}
//
// SQLiteDB.swift
// TasksGalore
//
// Created by Ilesh
// Copyright (c) 2019. All rights reserved.
//
import Foundation
// MARK:- SQLiteDB Class
/// Simple wrapper class to provide basic SQLite database access.
@objc(SQLiteDB)
class SQLiteDB: SQLiteBase {
/// The SQLite database file name - defaults to `data.db`.
var DB_NAME = "data.db"
/// Does this database have CloudKit support for remote data saving?
var cloudEnabled = false {
didSet {
if cloudEnabled {
self.cloudDB = CloudDB.shared
} else {
self.cloudDB = nil
}
}
}
/// Singleton instance for access to the SQLiteDB class
static let shared = SQLiteDB()
/// Internal reference to CloudDB instance
private var cloudDB: CloudDB!
private override init() {
super.init()
}
/// Output the current SQLite database path
override var description:String {
return "SQLiteDB: \(String(describing: path))"
}
// MARK:- Public Methods
/// Open the database specified by the `DB_NAME` variable and assigns the internal DB references. If a database is currently open, the method first closes the current database and gets a new DB references to the current database pointed to by `DB_NAME`
///
/// - Parameter copyFile: Whether to copy the file named in `DB_NAME` from resources or to create a new empty database file. Defaults to `true`
/// - Returns: Returns a boolean value indicating if the database was successfully opened or not.
override func open(dbPath: String = "", copyFile: Bool = true) -> Bool {
var dbURL: URL
if dbPath.isEmpty {
guard let url = Bundle.main.resourceURL else { return false }
dbURL = url.appendingPathComponent(DB_NAME)
} else {
dbURL = URL(fileURLWithPath: dbPath)
}
return super.open(dbPath: dbURL.path, copyFile: copyFile)
}
/// Close the currently open SQLite database. Before closing the DB, the framework automatically takes care of optimizing the DB at frequent intervals by running the following commands:
/// 1. **VACUUM** - Repack the DB to take advantage of deleted data
/// 2. **ANALYZE** - Gather information about the tables and indices so that the query optimizer can use the information to make queries work better.
override func closeDB() {
if db != nil {
// Get launch count value
let ud = UserDefaults.standard
var launchCount = ud.integer(forKey:"LaunchCount")
launchCount -= 1
NSLog("SQLiteDB - Launch count \(launchCount)")
var clean = false
if launchCount < 0 {
clean = true
launchCount = 500
}
ud.set(launchCount, forKey:"LaunchCount")
ud.synchronize()
// Do we clean DB?
if !clean {
sqlite3_close(db)
return
}
// Clean DB
NSLog("SQLiteDB - Optimize DB")
let sql = "VACUUM; ANALYZE"
if CInt(execute(sql:sql)) != SQLITE_OK {
NSLog("SQLiteDB - Error cleaning DB")
}
super.closeDB()
}
}
/// Create a record zone in the private DB for the given table
/// - Parameter version: An integer value indicating the new DB version.
func createCloudZone(table: SQLTable, completion: @escaping ()->Void) {
cloudDB.creaeZone(table: table) {
completion()
}
}
func getCloudUpdates(table: SQLTable) {
cloudDB.getUpdates(table: table)
}
/// Save data to the cloud via CloudKit
/// - Parameters:
/// - row: The SQLTable instance to be saved remotely.
/// - dbOverride: A `DBType` indicating the database to save the remote data to. If set, this overrides the database set by default for the table via the `remoteDB` method. Defaults to `none`.
func saveToCloud(row: SQLTable, dbOverride: DBType = .none) {
if !cloudEnabled {
return
}
// Save to cloude
cloudDB.saveToCloud(row: row)
}
}
//
// SQLTable.swift
// SQLiteDB-iOS
//
// Created by Ilesh.
// Copyright © 2019. All rights reserved.
//
import Foundation
/// Enumerator to be used in fetching data via some methods where you might need to specify whether you want all records, only records marked for deletion, or only records not marked for deletion.
enum FetchType: Int {
case all, deleted, nondeleted
}
protocol SQLTableProtocol {}
// MARK:- SQLiteDB Class
/// Base class for providing object-based access to SQLite tables. Simply define the properties and their default values (a value has to be there in order to determine value type) and SQLTable will handle the basic CRUD (creating, reading, updating, deleting) actions for you without any additional code.
@objcMembers
class SQLTable: NSObject, SQLTableProtocol {
/// Every SQLTable sub-class will contain an `isDeleted` flag. Instead of deleting records, you should set the flag to `true` for deletions and filter your data accordingly when fetching data from `SQLTable`. This flag will be used to synchronize deletions via CloudKit
public var isDeleted = false
/// Every SQLTable sub-class will contain a `created` property indicating the creation date of the record.
public var created = Date()
/// Every SQLTable sub-class will contain a `modified` property indicating the last modification date of the record.
public var modified = Date()
/// Internal reference to the SQLite table name as determined based on the name of the `SQLTable` sub-class name. The sub-class name should be in the singular - for example, Task for a tasks table.
internal var table = ""
/// Internal dictionary to keep track of whether a specific table was verfied to be in existence in the database. This dictionary is used to automatically create the table if it does not exist in the DB.
private static var verified = [String:Bool]()
/// Internal pointer to the main database
internal var db = SQLiteDB.shared
/// Base initialization which sets up the table name and then verifies that the table exists in the DB, and if it does not, creates it.
required override init() {
super.init()
// Table name
self.table = type(of: self).table
let verified = SQLTable.verified[table]
if verified == nil || !verified! {
// Verify that the table exists in DB
var sql = "SELECT name FROM sqlite_master WHERE type='table' AND lower(name)='\(table)'"
let cnt = db.query(sql:sql).count
if cnt == 1 {
// Table exists, verify strutcure and then proceed
verifyStructure()
SQLTable.verified[table] = true
} else if cnt == 0 {
// Table does not exist, create it
sql = "CREATE TABLE IF NOT EXISTS \(table) ("
// Columns
let cols = values()
var first = true
for col in cols {
if first {
first = false
sql += getColumnSQL(column:col)
} else {
sql += ", " + getColumnSQL(column: col)
}
}
// Close query
sql += ")"
let rc = db.execute(sql:sql)
if rc == 0 {
assert(false, "Error creating table - \(table) with SQL: \(sql)")
}
SQLTable.verified[table] = true
} else {
assert(false, "Got more than one table in DB with same name! Count: \(cnt) for \(table)")
}
// Create CloudKit zone, if necessary
if db.cloudEnabled {
if remoteDB() == DBType.privateDB {
db.createCloudZone(table: self) {
self.db.getCloudUpdates(table: self)
}
} else {
db.getCloudUpdates(table: self)
}
}
}
}
// MARK:- Table property management
/// The primary key for the table - defaults to `id`. Override this in `SQLTable` sub-classes to define a different column name as the primary key.
///
/// - Returns: A string indicating the name of the primary key column for the table. Defaults to `id`.
func primaryKey() -> String {
return "id"
}
/// The remote key for the table for data saved to CloudKit - defaults to `ckid`. Override this in `SQLTable` sub-classes to define a different column name as the remote key.
///
/// - Returns: A string indicating the name of the remote key column for the table. Defaults to `ckid`.
func remoteKey() -> String {
return "ckid"
}
/// The remote database for the table for data saved to CloudKit - defaults to `private`. Override this in `SQLTable` sub-classes to define a different database for a specific table.
///
/// - Returns: A `DBType` enum indicating the remote database for the table. Defaults to `private`.
func remoteDB() -> DBType {
return DBType.privateDB
}
/// An array of property names (in a sub-classed instance of `SQLTable`) that are to be ignored when fetching/saving information to the DB. Override this method in sub-classes when you have properties that you don't want persisted to the database.
///
/// - Returns: An array of String values indicating property/value names to be ignored when persisting data to the database.
func ignoredKeys() -> [String] {
return []
}
// MARK:- Class Methods
/// Returns a WHERE clause, or an empty string, depending on the passed in `FetchType`.
///
/// - Paramter type: The type of fetch operation to be performed.
/// - Returns: A String for the SQL WHERE clause, or an empty string if there is no WHERE clause.
class func whereFor(type: FetchType) -> String {
switch type {
case .all:
return ""
case .deleted:
return " WHERE isDeleted"
case .nondeleted:
return " WHERE (NOT isDeleted OR isDeleted IS NULL)"
}
}
/// Return the count of rows in the table, or the count of rows matching a specific filter criteria, if one was provided.
///
/// - Parameters:
/// - filter: The optional filter criteria to be used in fetching the data. Specify the filter criteria in the form of a valid SQLite WHERE clause (but without the actual WHERE keyword). If this parameter is omitted or a blank string is provided, the count of all rows, deleted rows, or non-deleted rows (depending on the `type` parameter) will be returned.
/// - type: The type of records to fetch. Defined via the `FetchType` enumerator and defaults to `nondeleted`.
/// - Returns: An integer value indicating the total number of rows, if no filter criteria was provided, or the number of rows matching the provided filter criteria.
class func count(filter: String = "", fetch: FetchType = .nondeleted) -> Int {
let db = SQLiteDB.shared
var sql = "SELECT COUNT(*) AS count FROM \(table)"
let wsql = SQLTable.whereFor(type: fetch)
if filter.isEmpty {
sql += wsql
} else {
if wsql.isEmpty {
sql += " WHERE \(filter)"
} else {
sql += wsql + " AND \(filter)"
}
}
let arr = db.query(sql:sql)
if arr.count == 0 {
return 0
}
if let val = arr[0]["count"] as? Int {
return val
}
return 0
}
/// Remove all the rows in the underlying table, or just the rows matching a provided criteria.
///
/// - Parameters:
/// - filter: The optional filter criteria to be used in removing data rows. Specify the filter criteria in the form of a valid SQLite WHERE clause (but without the actual WHERE keyword). If this parameter is omitted or a blank string is provided, all rows will be deleted from the underlying table.
/// - force: Flag indicating whether to force delete the records or simply mark them as deleted. Defaluts to `false`.
/// - Returns: A boolean value indicating whether the row deletion was successful or not.
class func remove(filter: String = "", force: Bool = false) -> Bool {
let db = SQLiteDB.shared
var params: [Any]? = [true, Date()]
var sql = "UPDATE \(table) SET isDeleted = ?, modified = ?"
if force {
params = nil
sql = "DELETE FROM \(table)"
}
if !filter.isEmpty {
// Use filter to delete
sql += " WHERE \(filter)"
}
let rc = db.execute(sql:sql, parameters: params)
return (rc != 0)
}
/// Remove all records marked as deleted.
///
/// Parameter filter: The optional filter criteria to be used in removing data rows. Specify the filter criteria in the form of a valid SQLite WHERE clause (but without the actual WHERE keyword). If this parameter is omitted or a blank string is provided, all rows marked as deleted will be removed from the underlying table.
class func clearTrash(filter: String = "") {
let db = SQLiteDB.shared
var sql = "DELETE FROM \(table) WHERE isDeleted"
if !filter.isEmpty {
// Use filter to delete
sql += " AND \(filter)"
}
_ = db.execute(sql:sql)
}
/// Remove all rows from the underlying table to create an empty table.
class func zap() {
let db = SQLiteDB.shared
let sql = "DELETE FROM \(table)"
_ = db.execute(sql:sql)
}
// MARK:- Public Methods
/// Save the current values for this particular `SQLTable` sub-class instance to the database.
///
/// - Parameters:
/// - updateCloud: A boolean indicating whether the save operation should save to the cloud as well. Defaults to `true`.
/// - dbOverride: A `DBType` indicating the database to save the remote data to. If set, this overrides the database set by default for the table via the `remoteDB` method. Defaults to `none`.
/// - Returns: An integer value indicating either the row id (in case of an insert) or the status of the save - a non-zero value indicates success and a 0 indicates failure.
func save(updateCloud: Bool = true, dbOverride: DBType = .none, strModifyDate:String = "") -> Int {
let key = primaryKey()
var data = values()
var insert = true
if let rid = data[key] {
var val = "\(rid)"
if rid is String {
val = "'\(rid)'"
}
let sql = "SELECT COUNT(*) AS count FROM \(table) WHERE \(primaryKey())=\(val)"
let arr = db.query(sql:sql)
if arr.count == 1 {
if let cnt = arr[0]["count"] as? Int {
insert = (cnt == 0)
}
}
}
// Insert or update
self.modified = Date()
if strModifyDate != "" {
data["modified"] = strModifyDate
}
let (sql, params) = getSQL(data:data, forInsert:insert)
let rc = db.execute(sql:sql, parameters:params)
if rc == 0 {
NSLog("Error saving record!")
return 0
}
// Do cloud update - check (as to whether to save to cloud is done by DB)
if updateCloud {
db.saveToCloud(row: self)
}
// Update primary key
let pid = data[key]
if insert {
if pid is Int64 {
setValue(rc, forKey:key)
} else if pid is Int {
setValue(Int(rc), forKey:key)
}
}
return rc
}
// MARK:- Public Methods
/// saveTransaction the current values for this particular `SQLTable` sub-class instance to the database.
///
/// - Parameters:
/// - updateCloud: A boolean indicating whether the save operation should save to the cloud as well. Defaults to `true`.
/// - dbOverride: A `DBType` indicating the database to save the remote data to. If set, this overrides the database set by default for the table via the `remoteDB` method. Defaults to `none`.
/// - Returns: An integer value indicating either the row id (in case of an insert) or the status of the save - a non-zero value indicates success and a 0 indicates failure.
func saveTransaction(arrParams:[[String:Any]], updateCloud: Bool = true, dbOverride: DBType = .none, isProperty :Bool = true) -> Int {
let key = primaryKey()
let data = values()
var insert = true
if let rid = data[key] {
var val = "\(rid)"
if rid is String {
val = "'\(rid)'"
}
let sql = "SELECT COUNT(*) AS count FROM \(table) WHERE \(primaryKey())=\(val)"
let arr = db.query(sql:sql)
if arr.count == 1 {
if let cnt = arr[0]["count"] as? Int {
insert = (cnt == 0)
}
}
}
// Insert or update
self.modified = Date()
let (sql, params) = getSQLTransactionValues(arrData:arrParams, forInsert:insert,isProperty: isProperty)
let rc = db.executeTransaction(sql: sql, parameters: params)
if rc == 0 {
NSLog("Error saving record!")
return 0
}
// Do cloud update - check (as to whether to save to cloud is done by DB)
if updateCloud {
db.saveToCloud(row: self)
}
// Update primary key
let pid = data[key]
if insert {
if pid is Int64 {
setValue(rc, forKey:key)
} else if pid is Int {
setValue(Int(rc), forKey:key)
}
}
return rc
}
// MARK:- Public Methods
/// saveAllRecords the current values for this particular `SQLTable` sub-class instance to the database.
///
func saveAllRecords(arrParams:[[String:Any]], updateCloud: Bool = true, dbOverride: DBType = .none) -> Int {
let key = primaryKey()
let data = values()
var insert = true
if let rid = data[key] {
var val = "\(rid)"
if rid is String {
val = "'\(rid)'"
}
let sql = "SELECT COUNT(*) AS count FROM \(table) WHERE \(primaryKey())=\(val)"
let arr = db.query(sql:sql)
if arr.count == 1 {
if let cnt = arr[0]["count"] as? Int {
insert = (cnt == 0)
}
}
}
// Insert or update
self.modified = Date()
let (sql, params) = getSQL_AllValues(arrData: arrParams, forInsert: insert)
let rc = db.executeTransaction(sql: sql, parameters: params)
if rc == 0 {
NSLog("Error saving record!")
return 0
}
// Do cloud update - check (as to whether to save to cloud is done by DB)
if updateCloud {
db.saveToCloud(row: self)
}
// Update primary key
let pid = data[key]
if insert {
if pid is Int64 {
setValue(rc, forKey:key)
} else if pid is Int {
setValue(Int(rc), forKey:key)
}
}
return rc
}
// MARK:- Public Methods
/// saveAllRecords the current values for this particular `SQLTable` sub-class instance to the database.
///
func saveAllRecordsWithModified(arrParams:[[String:Any]], updateCloud: Bool = true, dbOverride: DBType = .none) -> Int {
let key = primaryKey()
let data = values()
var insert = true
if let rid = data[key] {
var val = "\(rid)"
if rid is String {
val = "'\(rid)'"
}
let sql = "SELECT COUNT(*) AS count FROM \(table) WHERE \(primaryKey())=\(val)"
let arr = db.query(sql:sql)
if arr.count == 1 {
if let cnt = arr[0]["count"] as? Int {
insert = (cnt == 0)
}
}
}
// Insert or update
self.modified = Date()
let (sql, params) = getSQL_AllValuesModified(arrData: arrParams, forInsert: insert)
let rc = db.executeTransaction(sql: sql, parameters: params)
if rc == 0 {
NSLog("Error saving record!")
return 0
}
// Do cloud update - check (as to whether to save to cloud is done by DB)
if updateCloud {
db.saveToCloud(row: self)
}
// Update primary key
let pid = data[key]
if insert {
if pid is Int64 {
setValue(rc, forKey:key)
} else if pid is Int {
setValue(Int(rc), forKey:key)
}
}
return rc
}
/// Delete the row for this particular `SQLTable` sub-class instance from the database.
///
/// - Parameter force: Flag indicating whether to force delete the records or simply mark them as deleted. Defaluts to `false`.
/// - Returns: A boolean value indicating the success or failure of the operation.
func delete(force: Bool = false) -> Bool {
let key = primaryKey()
let data = values()
if let rid = data[key] {
var params: [Any]? = [true, Date()]
var sql = "UPDATE \(table) SET isDeleted = ?, modified = ? WHERE \(primaryKey())=\(rid)"
if force {
params = nil
sql = "DELETE FROM \(table) WHERE \(primaryKey())=\(rid)"
}
let rc = db.execute(sql:sql, parameters: params)
return (rc != 0)
}
return false
}
/// Update the data for this particular `SQLTable` sub-class instance from the database so that all values are updated with the latest values from the database.
func refresh() {
let key = primaryKey()
let data = values()
if let rid = data[key] {
let sql = "SELECT * FROM \(table) WHERE \(primaryKey())=\(rid)"
let arr = db.query(sql:sql)
for (key, _) in data {
if let val = arr[0][key] {
setValue(val, forKey:key)
}
}
}
}
// MARK:- Internal Methods
/// Fetch a dictionary of property names and their corresponding values that are supposed to be persisted to the underlying table. Any property names returned via the `ignoredKeys` method will be left out of the dictionary.
///
/// - Returns: A dictionary of property names and their corresponding values.
internal func values() -> [String:Any] {
var res = [String:Any]()
let obj = Mirror(reflecting:self)
processMirror(obj: obj, results: &res)
// Add super-class properties via recursion
getValues(obj: obj.superclassMirror, results: &res)
return res
}
// MARK:- Private Methods
/// Recursively walk down the super-class hierarchy to get all the properties for a `SQLTable` sub-class instance
///
/// - Parameters:
/// - obj: The `Mirror` instance for the super-class.
/// - results: A dictionary of properties and values which will be modified in-place.
private func getValues(obj: Mirror?, results: inout [String:Any]) {
guard let obj = obj else { return }
processMirror(obj: obj, results: &results)
// Call method recursively
getValues(obj: obj.superclassMirror, results: &results)
}
/// Creates a dictionary of property names and values based on a `Mirror` instance of an object.
///
/// - Parameters:
/// - obj: The `Mirror` instance to be used.
/// - results: A dictionary of properties and values which will be modified in-place.
private func processMirror(obj: Mirror, results: inout [String: Any]) {
for (_, attr) in obj.children.enumerated() {
if let name = attr.label {
// Ignore the table and db properties used internally
if name == "table" || name == "db" {
continue
}
// Ignore special properties and lazy vars
if ignoredKeys().contains(name) || name.hasSuffix(".storage") {
continue
}
results[name] = attr.value
}
}
}
/// Verify the structure of the underlying SQLite table and add any missing columns to the table as per the `SQLTable` sub-class definition.
private func verifyStructure() {
// Get table structure
var sql = "PRAGMA table_info(\(table));"
let arr = db.query(sql:sql)
// Extract column names
var columns = [String]()
for row in arr {
if let txt = row["name"] as? String {
columns.append(txt)
}
}
// Get SQLTable columns
let cols = values()
let names = cols.keys
// Validate the SQLTable columns exist in actual DB table
for nm in names {
if columns.contains(nm) {
continue
}
// Add missing column
if let val = cols[nm] {
let col = (key: nm, value:val)
sql = "ALTER TABLE \(table) ADD COLUMN " + getColumnSQL(column: col)
_ = db.execute(sql: sql)
}
}
}
/// Returns a valid SQL statement and matching list of bound parameters needed to insert a new row into the database or to update an existing row of data.
///
/// - Parameters:
/// - data: A dictionary of property names and their corresponding values that need to be persisted to the underlying table.
/// - forInsert: A boolean value indicating whether this is an insert or update action.
/// - Returns: A tuple containing a valid SQL command to persist data to the underlying table and the bound parameters for the SQL command, if any.
private func getSQL(data:[String:Any], forInsert:Bool = true) -> (String, [Any]?) {
var sql = ""
var params:[Any]? = nil
if forInsert {
// INSERT INTO tasks(task, categoryID) VALUES ('\(txtTask.text)', 1)
sql = "INSERT INTO \(table)("
} else {
// UPDATE tasks SET task = ? WHERE categoryID = ?
sql = "UPDATE \(table) SET "
}
let pkey = primaryKey()
var wsql = ""
var rid:Any?
var first = true
let sortData = data.sorted(by: {$0.0 < $1.0})
for (key, val) in sortData {
// Primary key handling
if pkey == key {
if forInsert {
if val is Int && (val as! Int) == -1 {
// Do not add this since this is (could be?) an auto-increment value
continue
}
} else {
// Update - set up WHERE clause
wsql += " WHERE " + key + " = ?"
rid = val
continue
}
}
// Set up parameter array - if we get here, then there are parameters
if first && params == nil {
params = [AnyObject]()
}
if forInsert {
sql += first ? "\(key)" : ", \(key)"
wsql += first ? " VALUES (?" : ", ?"
params!.append(val)
} else {
sql += first ? "\(key) = ?" : ", \(key) = ?"
params!.append(val)
}
first = false
}
// Finalize SQL
if forInsert {
sql += ")" + wsql + ")"
} else if params != nil && !wsql.isEmpty {
sql += wsql
params!.append(rid!)
}
// NSLog("Final SQL: \(sql) with parameters: \(params)")
return (sql, params)
}
/// Returns a valid SQL statement and matching list of bound parameters needed to insert a new row into the database or to update an existing row of data.
///
/// - Parameters:
/// - data: A dictionary of property names and their corresponding values that need to be persisted to the underlying table.
/// - forInsert: A boolean value indicating whether this is an insert or update action.
/// - Returns: A tuple containing a valid SQL command to persist data to the underlying table and the bound parameters for the SQL command, if any.
private func getSQLTransactionValues(arrData:[[String:Any]], forInsert:Bool = true, isProperty:Bool = false) -> (String, [[Any]]?) {
var sql = ""
var allParams: [[Any]] = []
if forInsert {
// INSERT INTO tasks(task, categoryID) VALUES ('\(txtTask.text)', 1)
sql = "INSERT INTO \(table)("
} else {
// UPDATE tasks SET task = ? WHERE categoryID = ?
sql = "UPDATE \(table) SET "
}
let pkey = primaryKey()
var isQuery: Bool = false
for element in arrData {
var params:[Any]? = nil
var wsql = ""
var rid:Any?
var first = true
let data = element.sorted(by: {$0.0 < $1.0})
for (key, val) in data {
if !isProperty {
if key != "refPaymentPlanId" &&
key != "InstallmentQuarterly" &&
key != "DepositTotal" &&
key != "PaymentPlan" &&
key != "refPropertyId" &&
key != "PaymentPlan_YearsNumber" &&
key != "DepositTotalPercent" {
continue
}
}else {
if key != "BathsNumber" &&
key != "Building" &&
key != "Cluster" &&
key != "CodeDeveloperFinal" &&
key != "CodeReseller" &&
key != "DesignCode" &&
key != "Developer" &&
key != "FeeClubHouse" &&
key != "FeeGaragePrice" &&
key != "FeeMaintenance" &&
key != "Floor" &&
key != "Furnishing" &&
key != "GarageSlotsNumber" &&
key != "Id" &&
key != "InventoryDate" &&
key != "IsActive" &&
key != "MaidsNumber" &&
key != "PaymentCodeList" &&
key != "Plot" &&
key != "PriceCash" &&
key != "PriceCashPerMeter" &&
key != "PriceChange" &&
key != "PriceChangePercentage" &&
key != "PriceFinish" &&
key != "PriceFinishPerMeter" &&
key != "PricePerMeter" &&
key != "PriceFirstPerMeter" &&
key != "PriceBase" && //PriceTotal = PriceBase
key != "PriceBaseFirst" && //PriceTotalFirst = PriceBaseFirst
key != "Project" &&
key != "Project_Area" &&
key != "Project_City" &&
key != "Project_Country" &&
key != "Project_District" &&
key != "ProjectPhase" &&
key != "PropertyCategory" &&
key != "PropertyFinishingType" &&
key != "PropertyStatus" &&
key != "PropertyType" &&
key != "RecordCreated" &&
key != "RecordCreatedBy" &&
key != "RecordUpdated" &&
key != "RecordUpdatedBy" &&
key != "refProjectId" &&
key != "refProjectPhaseId" &&
key != "refPropertyCategoryId" &&
key != "refPropertyFinishingTypeId" &&
key != "refPropertyStatusId" &&
key != "refPropertyTypeId" &&
key != "refSupplierId" &&
key != "refSupplierId_Developer" &&
key != "Remark" &&
key != "RoomsNumber" &&
key != "Selected" &&
key != "SpaceBasement" &&
key != "SpaceBuildUp" &&
key != "SpaceGarden" &&
key != "SpaceLand" &&
key != "SpacePenthouse" &&
key != "SpaceRoof" &&
key != "SpaceTerrace" &&
key != "Supplier" &&
key != "SourceMarket" &&
key != "WebAddress" &&
key != "Zone" &&
key != "DeliveryEstimated" &&
key != "PropertyView" &&
key != "PropertyFacing" &&
key != "PropertyFurnishing" &&
key != "refBrokerId" &&
key != "BrokerName" &&
key != "PropertyPriceTag" &&
key != "PriceStorage" &&
key != "PriceOutdoor" &&
key != "Storage" &&
key != "MaidsNumber" &&
key != "GarageSlotsNumber" &&
key != "Pool" &&
key != "Elevator" &&
key != "DriversNumber" &&
key != "PriceTotal" && //After live 2.2
key != "FeeSellerPercentage" &&
key != "FeeSeller" &&
key != "FeeBuyerPercentage" &&
key != "FeeBuyer" &&
key != "PriceContract" &&
key != "ContractDate" &&
key != "Premium" &&
key != "Referral" &&
key != "OwnerPhone" &&
key != "OwnerName" &&
key != "OwnerEmail" &&
key != "FeeTransfer" &&
key != "FeeTransferPercentage" &&
key != "PaidAmount" &&
key != "AmountRemaining" &&
key != "RemarkBroker" &&
key != "DepositRequired" &&
key != "Entrance" &&
key != "TypeSpaces" &&
key != "TypeFacility" &&
key != "PropertySubType" &&
key != "RecordUpdatedFinal" {
continue
//Property/SourceMarket Instead of Supplier_MarketType <-- 01/07/2019 Client feedback on skype.
}
}
if pkey == key {
if forInsert {
if val is Int && (val as! Int) == -1 {
// Do not add this since this is (could be?) an auto-increment value
continue
}
} else {
// Update - set up WHERE clause
if !isProperty {
wsql += " WHERE " + key + " = ?"
}else{
wsql += " WHERE " + key.coloum + " = ?"
}
rid = val
continue
}
}
// Set up parameter array - if we get here, then there are parameters
if first && params == nil {
params = [AnyObject]()
}
if forInsert {
if !isQuery {
if !isProperty {
sql += first ? "\(key)" : ", \(key)"
}else{
sql += first ? "\(key.coloum)" : ", \(key.coloum)"
}
wsql += first ? " VALUES (?" : ", ?"
}
params!.append(val)
} else {
if !isQuery {
if !isProperty {
sql += first ? "\(key) = ?" : ", \(key) = ?"
}else{
sql += first ? "\(key.coloum) = ?" : ", \(key.coloum) = ?"
}
}
params!.append(val)
}
first = false
}
// Finalize SQL
if forInsert && !isQuery{
sql += ")" + wsql + ")"
isQuery = true
} else if params != nil && !wsql.isEmpty{
if !isQuery {
sql += wsql
}
params!.append(rid!)
isQuery = true
}
// NSLog("Final SQL: \(sql) with parameters: \(params)")
if params != nil{
allParams.append(params!)
}
}
return (sql, allParams)
}
/// Returns a valid SQL statement and matching list of bound parameters needed to insert a new row into the database or to update an existing row of data.
///
/// - Parameters:
/// - data: A dictionary of property names and their corresponding values that need to be persisted to the underlying table.
/// - forInsert: A boolean value indicating whether this is an insert or update action.
/// - Returns: A tuple containing a valid SQL command to persist data to the underlying table and the bound parameters for the SQL command, if any.
private func getSQL_AllValues(arrData:[[String:Any]], forInsert:Bool = true) -> (String, [[Any]]?) {
var sql = ""
var allParams: [[Any]] = []
if forInsert {
// INSERT INTO tasks(task, categoryID) VALUES ('\(txtTask.text)', 1)
sql = "INSERT INTO \(table)("
} else {
// UPDATE tasks SET task = ? WHERE categoryID = ?
sql = "UPDATE \(table) SET "
}
let pkey = primaryKey()
var isQuery: Bool = false
for element in arrData {
var params:[Any]? = nil
var wsql = ""
var rid:Any?
var first = true
let data = element.sorted(by: {$0.0 < $1.0})
let arrProperties = self.propertyNames()
for (key, val) in data {
if !arrProperties.contains(key){
continue
}
if pkey == key {
if forInsert {
if val is Int && (val as! Int) == -1 {
// Do not add this since this is (could be?) an auto-increment value
continue
}
} else {
// Update - set up WHERE clause
wsql += " WHERE " + key + " = ?"
rid = val
continue
}
}
// Set up parameter array - if we get here, then there are parameters
if first && params == nil {
params = [AnyObject]()
}
if forInsert {
if !isQuery {
sql += first ? "\(key)" : ", \(key)"
wsql += first ? " VALUES (?" : ", ?"
}
params!.append(val)
} else {
if !isQuery {
sql += first ? "\(key) = ?" : ", \(key) = ?"
}
params!.append(val)
}
first = false
}
// Finalize SQL
if forInsert && !isQuery{
sql += ")" + wsql + ")"
isQuery = true
} else if params != nil && !wsql.isEmpty{
if !isQuery {
sql += wsql
}
params!.append(rid!)
isQuery = true
}
// NSLog("Final SQL: \(sql) with parameters: \(params)")
if params != nil{
allParams.append(params!)
}
}
return (sql, allParams)
}
/// Returns a valid SQL statement and matching list of bound parameters needed to insert a new row into the database or to update an existing row of data.
///
/// - Parameters:
/// - data: A dictionary of property names and their corresponding values that need to be persisted to the underlying table.
/// - forInsert: A boolean value indicating whether this is an insert or update action.
/// - Returns: A tuple containing a valid SQL command to persist data to the underlying table and the bound parameters for the SQL command, if any.
private func getSQL_AllValuesModified(arrData:[[String:Any]], forInsert:Bool = true) -> (String, [[Any]]?) {
var sql = ""
var allParams: [[Any]] = []
if forInsert {
// INSERT INTO tasks(task, categoryID) VALUES ('\(txtTask.text)', 1)
sql = "INSERT INTO \(table)("
} else {
// UPDATE tasks SET task = ? WHERE categoryID = ?
sql = "UPDATE \(table) SET "
}
let pkey = primaryKey()
var isQuery: Bool = false
for element in arrData {
var params:[Any]? = nil
var wsql = ""
var rid:Any?
var first = true
let data = element.sorted(by: {$0.0 < $1.0})
for (key, val) in data {
if pkey == key {
if forInsert {
if val is Int && (val as! Int) == -1 {
// Do not add this since this is (could be?) an auto-increment value
continue
}
} else {
// Update - set up WHERE clause
wsql += " WHERE " + key + " = ?"
rid = val
continue
}
}
// Set up parameter array - if we get here, then there are parameters
if first && params == nil {
params = [AnyObject]()
}
if forInsert {
if !isQuery {
sql += first ? "\(key)" : ", \(key)"
wsql += first ? " VALUES (?" : ", ?"
}
params!.append(val)
} else {
if !isQuery {
sql += first ? "\(key) = ?" : ", \(key) = ?"
}
params!.append(val)
}
first = false
}
// Finalize SQL
if forInsert && !isQuery{
sql += ")" + wsql + ")"
isQuery = true
} else if params != nil && !wsql.isEmpty{
if !isQuery {
sql += wsql
}
params!.append(rid!)
isQuery = true
}
// NSLog("Final SQL: \(sql) with parameters: \(params)")
if params != nil{
allParams.append(params!)
}
}
return (sql, allParams)
}
/// Returns a valid SQL fragment for creating the columns, with the correct data type, for the underlying table.
///
/// - Parameter columns: A dictionary of property names and their corresponding values for the `SQLTable` sub-class
/// - Returns: A string containing an SQL fragment for delcaring the columns for the underlying table with the correct data type
private func getColumnSQL(column:(key: String, value: Any)) -> String {
let key = column.key
let val = column.value
var sql = "'\(key)' "
if val is Int {
// Integers
sql += "INTEGER"
if key == primaryKey() {
sql += " PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE"
} else {
sql += " DEFAULT \(val)"
}
} else {
// Other values
if val is Float || val is Double {
sql += "REAL DEFAULT \(val)"
} else if val is Bool {
sql += "BOOLEAN DEFAULT " + ((val as! Bool) ? "1" : "0")
} else if val is Date {
sql += "DATE"
// Cannot add a default when modifying a table, but can do so on new table creation
// if let dt = val as? Date {
// let now = Date()
// if now.timeIntervalSince(dt) < 3600 {
// sql += " DEFAULT current_timestamp"
// }
// }
} else if val is NSData {
sql += "BLOB"
} else {
// Default to text
sql += "TEXT"
}
if key == primaryKey() {
sql += " PRIMARY KEY NOT NULL UNIQUE"
}
}
return sql
}
}
extension SQLTableProtocol where Self: SQLTable {
/// Static variable indicating the table name - used in class methods since the instance variable `table` is not accessible in class methods.
static var table:String {
let cls = "\(classForCoder())".lowercased()
let ndx = cls.index(before:cls.endIndex)
_ = cls.hasSuffix("y") ? cls[..<ndx] + "ies" : (cls.hasSuffix("s") ? cls + "es" : cls + "s")
return cls
}
/// Return an array of values for an `SQLTable` sub-class (optionally) matching specified filter criteria, (optionally) in a given column order, and (optionally) limited to a specific number of rows.
///
/// - Parameters:
/// - filter: The optional filter criteria to be used in fetching the data. Specify the filter criteria in the form of a valid SQLite WHERE clause (but without the actual WHERE keyword). If this parameter is omitted or a blank string is provided, all rows will be fetched.
/// - order: The optional sort order for the data. Specify the sort order as valid SQLite statements as they would appear in an ORDER BY caluse (but without the ORDER BY part). If this parameter is omitted, or a blank string is provided, the data will not be ordered and will be retrieved in the order it was entered into the database.
/// - limit: The optional number of rows to fetch. If no value is provide or a 0 value is passed in, all rows will be fetched. Otherwise, up to "n" rows, where "n" is the number specified by the `limit` parameter, will be fetched depending on the other passed in parameters.
/// - type: The type of records to fetch. Defined via the `FetchType` enumerator and defaults to `nondeleted`.
/// - Returns: An array of `SQLTable` sub-class instances matching the criteria as specified in the `filter` and `limit` parameters orderd as per the `order` parameter.
static func rows(filter: String = "", order: String = "", limit: Int = 0, type: FetchType = .nondeleted) -> [Self] {
var sql = "SELECT * FROM \(table)"
let wsql = SQLTable.whereFor(type: type)
if filter.isEmpty {
sql += wsql
} else {
if wsql.isEmpty {
sql += " WHERE \(filter)"
} else {
sql += wsql + " AND \(filter)"
}
}
if !order.isEmpty {
sql += " ORDER BY \(order)"
}
if limit > 0 {
sql += " LIMIT 0, \(limit)"
}
return self.rowsFor(sql:sql)
}
/// Return an array of values for an `SQLTable` sub-class based on a passed in SQL query.
///
/// - Parameter sql: The SQL query to be used to fetch the data. This should be a valid (and complete) SQL query
/// - Returns: Returns an empty array if no matching rows were found. Otherwise, returns an array of `SQLTable` sub-class instances matching the criterias specified as per the SQL query passed in via the `sql` parameter. Returns any matching row, even if they are marked for deletion, unless the provided SQL query specifically excluded deleted records.
static func rowsFor(sql: String = "") -> [Self] {
var res = [Self]()
let tmp = self.init()
let data = tmp.values()
let db = SQLiteDB.shared
let fsql = sql.isEmpty ? "SELECT * FROM \(table)" : sql
let arr = db.query(sql:fsql)
for row in arr {
let t = self.init()
for (key, _) in data {
if let val = row[key] {
t.setValue(val, forKey:key)
}
}
res.append(t)
}
return res
}
/// Return an array of values for an `SQLTable` sub-class based on a passed in SQL query.
///
/// - Parameter sql: The SQL query to be used to fetch the data. This should be a valid (and complete) SQL query
/// - Returns: Returns an empty array if no matching rows were found. Otherwise, returns an array of `SQLTable` sub-class instances matching the criterias specified as per the SQL query passed in via the `sql` parameter. Returns any matching row, even if they are marked for deletion, unless the provided SQL query specifically excluded deleted records.
static func rowsForCounts(sql: String = "") -> Int {
let db = SQLiteDB.shared
let fsql = sql.isEmpty ? "SELECT * FROM \(table)" : sql
let arr = db.query(sql:fsql)
return (arr.first?.values.first as? Int ?? 0)
}
/// Return an array of values for an `SQLTable` sub-class based on a passed in SQL query.
///
/// - Parameter sql: The SQL query to be used to fetch the data. This should be a valid (and complete) SQL query
/// - Returns: Returns an empty array if no matching rows were found. Otherwise, returns an array of `SQLTable` sub-class instances matching the criterias specified as per the SQL query passed in via the `sql` parameter. Returns any matching row, even if they are marked for deletion, unless the provided SQL query specifically excluded deleted records.
static func rowsForDoubleValue(sql: String = "") -> Double {
let db = SQLiteDB.shared
let fsql = sql.isEmpty ? "SELECT * FROM \(table)" : sql
let arr = db.query(sql:fsql)
return (arr.first?.values.first as? Double ?? 0)
}
/// Return an array of values for an `SQLTable` sub-class based on a passed in SQL query.
///
/// - Parameter sql: The SQL query to be used to fetch the data. This should be a valid (and complete) SQL query
/// - Returns: Returns an value if found Otherwise zero.
static func rowsForValue(sql: String = "") -> Double {
let db = SQLiteDB.shared
let fsql = sql.isEmpty ? "SELECT * FROM \(table)" : sql
let arr = db.query(sql:fsql)
return (arr.first?.values.first as? Double ?? 0)
}
/// Return an instance of `SQLTable` sub-class for a given primary key value.
///
/// - Parameter id: The primary key value for the row of data you want to get.
/// - Returns: Return an instance of `SQLTable` sub-class if a matching row for the primary key was found, otherwise, returns nil. Returns any row, even if it is marked for deletion, as long as the provided ID matches.
static func rowBy(id: Any) -> Self? {
let row = self.init()
let data = row.values()
let db = SQLiteDB.shared
var val = "\(id)"
if id is String {
val = "'\(id)'"
}
let sql = "SELECT * FROM \(table) WHERE \(row.primaryKey())=\(val)"
let arr = db.query(sql:sql)
if arr.count == 0 {
return nil
}
for (key, _) in data {
if let val = arr[0][key] {
row.setValue(val, forKey:key)
}
}
return row
}
/// Return an instance of `SQLTable` sub-class for a given 0-based row number matching specific (optional) filtering and sorting criteria. Especially useful for fetching just one row to populate a `UITableView` as needed instead of populating a full array of data that you might (or might not) need.
///
/// - Parameters:
/// - number: 0-based row number, used mostly for accessing rows for display in UITableViews.
/// - filter: The optional filter criteria to be used in fetching the data. Specify the filter criteria in the form of a valid SQLite WHERE clause (but without the actual WHERE keyword). If this parameter is omitted or a blank string is provided, all rows will be fetched.
/// - order: The optional sort order for the data. Specify the sort order as valid SQLite statements as they would appear in an ORDER BY caluse (but without the ORDER BY part). If this parameter is omitted, or a blank string is provided, the data will not be ordered and will be retrieved in the order it was entered into the database.
/// - type: The type of records to fetch. Defined via the `FetchType` enumerator and defaults to `nondeleted`.
/// - Returns: Return an instance of `SQLTable` sub-class if a matching row for the provided row number and filter criteria was found, otherwise, returns nil.
static func row(number: Int, filter: String = "", order: String = "", type: FetchType = .nondeleted) -> Self? {
let row = self.init()
let data = row.values()
let db = SQLiteDB.shared
var sql = "SELECT * FROM \(table)"
let wsql = SQLTable.whereFor(type: type)
if filter.isEmpty {
sql += wsql
} else {
if wsql.isEmpty {
sql += " WHERE \(filter)"
} else {
sql += wsql + " AND \(filter)"
}
}
if !order.isEmpty {
sql += " ORDER BY \(order)"
}
// Limit to specified row
sql += " LIMIT 1 OFFSET \(number)"
let arr = db.query(sql:sql)
if arr.count == 0 {
return nil
}
for (key, _) in data {
if let val = arr[0][key] {
row.setValue(val, forKey:key)
}
}
return row
}
func propertyNames() -> [String] {
return Mirror(reflecting: self).children.compactMap{ $0.label }
}
}
extension String {
var coloum : String {
if self == "DeliveryEstimated" {
return "deliveryDate"
}
else if self == "Zone" {
return "zone_"
}else if self == "WebAddress" {
return "webAddress"
}else if self == "Supplier_MarketType" {
return "supplierMarketType"
}else if self == "Supplier" {
return "supplier"
}else if self == "SpaceTerrace" {
return "spaceTerrace"
}
else if self == "SpaceRoof" {
return "spaceRoof"
}
else if self == "SpacePenthouse" {
return "spacePenthouse"
}
else if self == "SpaceLand" {
return "spaceLand"
}else if self == "SpaceGarden" {
return "spaceGarden"
}
else if self == "SpaceBuildUp" {
return "spaceBuildUp"
}else if self == "SpaceBasement" {
return "spaceBasement"
}
else if self == "Selected" {
return "selected"
}else if self == "RoomsNumber" {
return "roomsNumber"
}else if self == "Remark" {
return "remark"
}else if self == "RecordUpdatedBy" {
return "recordUpdatedBy"
}else if self == "RecordUpdated" {
return "recordUpdated"
}else if self == "RecordCreatedBy" {
return "recordCreatedBy"
}else if self == "RecordCreated" {
return "recordCreated"
}else if self == "PropertyType" {
return "propertyType"
}else if self == "PropertyStatus" {
return "propertyStatus"
}else if self == "PropertyFinishingType" {
return "propertyFinishingType"
}else if self == "PropertyCategory" {
return "propertyCategory"
}else if self == "ProjectPhase" {
return "projectPhase"
}else if self == "Project_District" {
return "projectDistrict"
}else if self == "Project_Country" {
return "projectCountry"
}else if self == "Project_City" {
return "projectCity"
}else if self == "Project_Area" {
return "projectArea"
}else if self == "Project" {
return "project"
}else if self == "PriceBaseFirst" { //PriceTotalFirst = PriceBaseFirst [23/07/2019]
return "priceTotalFirst"
}else if self == "PriceFirstPerMeter" {
return "priceFirstPerMeter"
}else if self == "PricePerMeter" {
return "pricePerMeter"
}else if self == "PriceFinishPerMeter" {
return "priceFinishPerMeter"
}else if self == "PriceFinish" {
return "priceFinish"
}else if self == "PriceChangePercentage" {
return "priceChangePercentage"
}else if self == "PriceChange" {
return "priceChange"
}else if self == "PriceCashPerMeter" {
return "priceCashPerMeter"
}else if self == "PriceCash" {
return "priceCash"
}else if self == "Plot" {
return "plot"
}else if self == "PaymentCodeList" {
return "paymentCodeList"
}else if self == "MaidsNumber" {
return "maidsNumber"
}else if self == "IsActive" {
return "isActive"
}else if self == "InventoryDate" {
return "inventoryDate"
}else if self == "Id" {
return "propertyID"
}else if self == "GarageSlotsNumber" {
return "garageSlotsNumber"
}else if self == "Furnishing" {
return "furnishing"
}else if self == "Floor" {
return "floor"
}else if self == "FeeMaintenance" {
return "feeMaintenance"
}else if self == "FeeGaragePrice" {
return "feeGaragePrice"
}else if self == "FeeClubHouse" {
return "feeClubHouse"
}else if self == "DriversNumber" {
return "driversNumber"
}else if self == "Developer" {
return "developer"
}else if self == "DesignCode" {
return "designCode"
}else if self == "CodeReseller" {
return "codeReseller"
}else if self == "CodeDeveloperFinal" {
return "codeDeveloper"
}else if self == "Cluster" {
return "cluster"
}else if self == "Building" {
return "building"
}else if self == "BathsNumber" {
return "bathsNumber"
}else if self == "refSupplierId_Developer" {
return "refSupplierIdDeveloper"
}else if self == "SourceMarket" {
return "supplierMarketType"
}
return self
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment