Skip to content

Commit a091c46

Browse files
committed
Automatic DB and Table Creation Support
* Added ability to specify any name for the database * Added a public openDB method with the ability to specify whether to copy the DB from resources (default) or to create a new DB * Added support for checking that each table specified via SQLTable exists on first access and to automatically create the table structure if it doesn’t exist * Added asserts to verify that the DB is open before any queries are run * Minor code clean up in SQLTable * Updated iOS sample project to include the new openDB() method call
1 parent 1bae348 commit a091c46

File tree

4 files changed

+139
-87
lines changed

4 files changed

+139
-87
lines changed

Examples/OSX/Resources/Base.lproj/Main.storyboard

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="6198" systemVersion="14A297b" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
33
<dependencies>
4-
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6198"/>
4+
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
5+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
56
</dependencies>
67
<scenes>
78
<!--Application-->
@@ -641,12 +642,12 @@
641642
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
642643
</connections>
643644
</application>
644-
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
645+
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="SQLiteDB_OSX" customModuleProvider="target"/>
645646
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
646647
</objects>
647648
<point key="canvasLocation" x="75" y="0.0"/>
648649
</scene>
649-
<!--Window Controller - Window-->
650+
<!--Window Controller-->
650651
<scene sceneID="R2V-B0-nI4">
651652
<objects>
652653
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
@@ -667,7 +668,7 @@
667668
<!--View Controller-->
668669
<scene sceneID="hIz-AP-VOD">
669670
<objects>
670-
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
671+
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="SQLiteDB_OSX" customModuleProvider="target" sceneMemberID="viewController">
671672
<view key="view" id="m2S-Jp-Qdl">
672673
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
673674
<autoresizingMask key="autoresizingMask"/>

Examples/iOS/Source/AppDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
import UIKit
1010

1111
@UIApplicationMain
12-
class AppDelegate: UIResponder, UIApplicationDelegate {
12+
class AppDelegate:UIResponder, UIApplicationDelegate {
1313

1414
var window: UIWindow?
1515
let db = SQLiteDB.shared
1616

1717
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
18+
_ = db.openDB(copyFile:false)
1819
return true
1920
}
2021

SQLTable.swift

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
@objc(SQLTable)
1616
class SQLTable:NSObject {
1717
private var table = ""
18+
private static var verified = [String:Bool]()
1819

1920
private static var table:String {
2021
let cls = "\(classForCoder())".lowercased()
@@ -30,6 +31,32 @@ class SQLTable:NSObject {
3031
let ndx = cls.characters.index(before:cls.endIndex)
3132
let tnm = cls.hasSuffix("y") ? cls.substring(to:ndx) + "ies" : cls + "s"
3233
self.table = tnm
34+
let verified = SQLTable.verified[table]
35+
if verified == nil || !verified! {
36+
// Verify that the table exists in DB
37+
let db = SQLiteDB.shared
38+
var sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='\(table)'"
39+
let cnt = db.query(sql:sql).count
40+
if cnt == 1 {
41+
// Table exists, proceed
42+
SQLTable.verified[table] = true
43+
} else if cnt == 0 {
44+
// Table does not exist, create it
45+
sql = "CREATE TABLE IF NOT EXISTS \(table) ("
46+
// Columns
47+
let cols = values()
48+
sql += getColumnSQL(columns:cols)
49+
// Close query
50+
sql += ")"
51+
let rc = db.execute(sql:sql)
52+
if rc == 0 {
53+
assert(false, "Error creating table - \(table) with SQL: \(sql)")
54+
}
55+
SQLTable.verified[table] = true
56+
} else {
57+
assert(false, "Got more than one table in DB with same name! Count: \(cnt) for \(table)")
58+
}
59+
}
3360
}
3461

3562
// MARK:- Table property management
@@ -251,31 +278,12 @@ class SQLTable:NSObject {
251278
if ignoredKeys().contains(name) || name.hasSuffix(".storage") {
252279
continue
253280
}
254-
res[name] = get(value:attr.value)
281+
res[name] = attr.value
255282
}
256283
}
257284
return res
258285
}
259286

260-
private func get(value:Any) -> Any {
261-
if value is String {
262-
return value as! String
263-
} else if value is Int {
264-
return value as! Int
265-
} else if value is Float {
266-
return value as! Float
267-
} else if value is Double {
268-
return value as! Double
269-
} else if value is Bool {
270-
return value as! Bool
271-
} else if value is Date {
272-
return value as! Date
273-
} else if value is NSData {
274-
return value as! NSData
275-
}
276-
return "nAn"
277-
}
278-
279287
private func getSQL(data:[String:Any], forInsert:Bool = true) -> (String, [Any]?) {
280288
var sql = ""
281289
var params:[Any]? = nil
@@ -329,4 +337,42 @@ class SQLTable:NSObject {
329337
// NSLog("Final SQL: \(sql) with parameters: \(params)")
330338
return (sql, params)
331339
}
340+
341+
private func getColumnSQL(columns:[String:Any]) -> String {
342+
var sql = ""
343+
for key in columns.keys {
344+
let val = columns[key]!
345+
var col = "'\(key)' "
346+
if val is Int {
347+
// Integers
348+
col += "INTEGER"
349+
if key == primaryKey() {
350+
col += " PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE"
351+
}
352+
} else {
353+
// Other values
354+
if val is Float || val is Double {
355+
col += "REAL"
356+
} else if val is Bool {
357+
col += "BOOLEAN"
358+
} else if val is Date {
359+
col += "DATE"
360+
} else if val is NSData {
361+
col += "BLOB"
362+
} else {
363+
// Default to text
364+
col += "TEXT"
365+
}
366+
if key == primaryKey() {
367+
col += " PRIMARY KEY NOT NULL UNIQUE"
368+
}
369+
}
370+
if sql.isEmpty {
371+
sql = col
372+
} else {
373+
sql += ", " + col
374+
}
375+
}
376+
return sql
377+
}
332378
}

SQLiteDB.swift

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ private let SQLITE_TRANSIENT = unsafeBitCast(-1, to:sqlite3_destructor_type.self
2121
// MARK:- SQLiteDB Class - Does all the work
2222
@objc(SQLiteDB)
2323
class SQLiteDB:NSObject {
24-
let DB_NAME = "data.db"
24+
var DB_NAME = "data.db"
2525
let QUEUE_LABEL = "SQLiteDB"
2626
static let shared = SQLiteDB()
2727
private var db:OpaquePointer? = nil
@@ -31,61 +31,80 @@ class SQLiteDB:NSObject {
3131

3232
private override init() {
3333
super.init()
34+
// Set up essentials
35+
queue = DispatchQueue(label:QUEUE_LABEL, attributes:[])
36+
// 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
37+
fmt.locale = Locale(identifier:"en_US_POSIX")
38+
fmt.timeZone = TimeZone(secondsFromGMT:0)
39+
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
40+
}
41+
42+
deinit {
43+
closeDB()
44+
}
45+
46+
override var description:String {
47+
return "SQLiteDB: \(path)"
48+
}
49+
50+
// MARK:- Public Methods
51+
func openDB(copyFile:Bool = true) -> Bool {
52+
if db != nil {
53+
closeDB()
54+
}
3455
// Set up for file operations
3556
let fm = FileManager.default
3657
// Get path to DB in Documents directory
3758
var docDir = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
3859
// If macOS, add app name to path since otherwise, DB could possibly interfere with another app using SQLiteDB
39-
#if os(OSX)
40-
let info = Bundle.main.infoDictionary!
41-
let appName = info["CFBundleName"] as! String
42-
docDir = (docDir as NSString).appendingPathComponent(appName)
43-
// Create folder if it does not exist
44-
if !fm.fileExists(atPath:docDir) {
45-
do {
46-
try fm.createDirectory(atPath:docDir, withIntermediateDirectories:true, attributes:nil)
47-
} catch {
48-
assert(false, "SQLiteDB: Error creating DB directory: \(docDir) on macOS")
49-
return
60+
#if os(OSX)
61+
let info = Bundle.main.infoDictionary!
62+
let appName = info["CFBundleName"] as! String
63+
docDir = (docDir as NSString).appendingPathComponent(appName)
64+
// Create folder if it does not exist
65+
if !fm.fileExists(atPath:docDir) {
66+
do {
67+
try fm.createDirectory(atPath:docDir, withIntermediateDirectories:true, attributes:nil)
68+
} catch {
69+
assert(false, "SQLiteDB: Error creating DB directory: \(docDir) on macOS")
70+
return false
71+
}
5072
}
51-
}
52-
#endif
73+
#endif
5374
let path = (docDir as NSString).appendingPathComponent(DB_NAME)
54-
// Check if copy of DB is there in Documents directory
55-
if !(fm.fileExists(atPath:path)) {
56-
// The database does not exist, so copy to Documents directory
57-
guard let rp = Bundle.main.resourcePath else { return }
75+
// Check if DB is there in Documents directory
76+
if !(fm.fileExists(atPath:path)) && copyFile {
77+
// The database does not exist, so copy it
78+
guard let rp = Bundle.main.resourcePath else { return false }
5879
let from = (rp as NSString).appendingPathComponent(DB_NAME)
5980
do {
6081
try fm.copyItem(atPath:from, toPath:path)
6182
} catch let error {
6283
assert(false, "SQLiteDB: Failed to copy writable version of DB! Error - \(error.localizedDescription)")
63-
return
84+
return false
6485
}
6586
}
66-
openDB(path:path)
67-
}
68-
69-
private init(path:String) {
70-
super.init()
71-
openDB(path:path)
72-
}
73-
74-
deinit {
75-
closeDB()
76-
}
77-
78-
override var description:String {
79-
return "SQLiteDB: \(path)"
87+
// Open the DB
88+
let cpath = path.cString(using:String.Encoding.utf8)
89+
let error = sqlite3_open(cpath!, &db)
90+
if error != SQLITE_OK {
91+
// Open failed, close DB and fail
92+
NSLog("SQLiteDB - failed to open DB!")
93+
sqlite3_close(db)
94+
return false
95+
}
96+
NSLog("SQLiteDB opened!")
97+
return true
8098
}
8199

82-
// MARK:- Public Methods
100+
// Return an ISO-8601 date string
83101
func dbDate(dt:Date) -> String {
84102
return fmt.string(from:dt)
85103
}
86104

87105
// Execute SQL with parameters and return result code
88-
func execute(sql:String, parameters:[Any]?=nil)->Int {
106+
func execute(sql:String, parameters:[Any]? = nil)->Int {
107+
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
89108
var result = 0
90109
queue.sync {
91110
if let stmt = self.prepare(sql:sql, params:parameters) {
@@ -96,7 +115,8 @@ class SQLiteDB:NSObject {
96115
}
97116

98117
// Run SQL query with parameters
99-
func query(sql:String, parameters:[Any]?=nil)->[[String:Any]] {
118+
func query(sql:String, parameters:[Any]? = nil)->[[String:Any]] {
119+
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
100120
var rows = [[String:Any]]()
101121
queue.sync {
102122
if let stmt = self.prepare(sql:sql, params:parameters) {
@@ -108,6 +128,7 @@ class SQLiteDB:NSObject {
108128

109129
// Versioning
110130
func getDBVersion() -> Int {
131+
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
111132
var version = 0
112133
let arr = query(sql:"PRAGMA user_version")
113134
if arr.count == 1 {
@@ -118,34 +139,16 @@ class SQLiteDB:NSObject {
118139

119140
// Sets the 'user_version' value, a user-defined version number for the database. This is useful for managing migrations.
120141
func set(version:Int) {
142+
assert(db != nil, "Database has not been opened! Use the openDB() method before any DB queries.")
121143
_ = execute(sql:"PRAGMA user_version=\(version)")
122144
}
123145

124146
// MARK:- Private Methods
125-
private func openDB(path:String) {
126-
// Set up essentials
127-
queue = DispatchQueue(label:QUEUE_LABEL, attributes:[])
128-
// 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
129-
fmt.locale = Locale(identifier:"en_US_POSIX")
130-
fmt.timeZone = TimeZone(secondsFromGMT:0)
131-
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
132-
// Open the DB
133-
let cpath = path.cString(using: String.Encoding.utf8)
134-
let error = sqlite3_open(cpath!, &db)
135-
if error != SQLITE_OK {
136-
// Open failed, close DB and fail
137-
NSLog("SQLiteDB - failed to open DB!")
138-
sqlite3_close(db)
139-
return
140-
}
141-
NSLog("SQLiteDB opened!")
142-
}
143-
144147
private func closeDB() {
145148
if db != nil {
146149
// Get launch count value
147150
let ud = UserDefaults.standard
148-
var launchCount = ud.integer(forKey: "LaunchCount")
151+
var launchCount = ud.integer(forKey:"LaunchCount")
149152
launchCount -= 1
150153
NSLog("SQLiteDB - Launch count \(launchCount)")
151154
var clean = false
@@ -167,6 +170,7 @@ class SQLiteDB:NSObject {
167170
NSLog("SQLiteDB - Error cleaning DB")
168171
}
169172
sqlite3_close(db)
173+
self.db = nil
170174
}
171175
}
172176

@@ -188,9 +192,9 @@ class SQLiteDB:NSObject {
188192
if params != nil {
189193
// Validate parameters
190194
let cntParams = sqlite3_bind_parameter_count(stmt)
191-
let cnt = CInt(params!.count)
192-
if cntParams != cnt {
193-
let msg = "SQLiteDB - failed to bind parameters, counts did not match. SQL: \(sql), Parameters: \(params)"
195+
let cnt = params!.count
196+
if cntParams != CInt(cnt) {
197+
let msg = "SQLiteDB - failed to bind parameters, counts did not match. SQL: \(sql), Parameters: \(params!)"
194198
NSLog(msg)
195199
return nil
196200
}
@@ -220,7 +224,7 @@ class SQLiteDB:NSObject {
220224
if flag != SQLITE_OK {
221225
sqlite3_finalize(stmt)
222226
if let error = String(validatingUTF8:sqlite3_errmsg(self.db)) {
223-
let msg = "SQLiteDB - failed to bind for SQL: \(sql), Parameters: \(params), Index: \(ndx) Error: \(error)"
227+
let msg = "SQLiteDB - failed to bind for SQL: \(sql), Parameters: \(params!), Index: \(ndx) Error: \(error)"
224228
NSLog(msg)
225229
}
226230
return nil

0 commit comments

Comments
 (0)