A library for easier and type-safe use of sqlite in the apple device(ios/macos) environment
SQLiteService
It is a library for easier and type-safe use of sqlite in the apple device(ios/macos) environment.
Installation
Currently, only SPM is supported.
How to use it
The interface of SQLiteService simply consists of open/close + run + migration. The operation that returns the Result type is synchronous, and when the Result is passed to the completion handler, it operates asynchronously. (Synchronous operations of asynchronous operation + run action are executed after the migration operation is finished internally in SQLiteService.)
The run method should be called with a closure of this type: (DataBase) throws -> T
indicating what action to take and what the result type is. (DataBase)
follows the Connection & DataBase
protocol. Please refer to the protocol for which functions are supported. (Instead of using SQLiteService, you can directly handle SQLiteDataBase
objects that conforms the Connection & DataBase
protocol.)
open and close database
private func openDatabaseAndCloseExample() {
let openResult: Result<Void, Error> = self.service.open(path: self.dbPath)
print("db open result: \(openResult)")
let closeResult: Result<Void, Error> = self.service.close()
print("db close result: \(closeResult)")
self.service.open(path: self.dbPath) { result in
print("db open result: \(result)")
}
self.service.close { result in
print("db close result: \(result)")
}
}
Table
Table
must be defined with ColumnType
and EntityType
.
Use ColumnType
to specify the column name and stored data type.
EntityType
must conform to RowValueType
, which means that the query result can be returned as a data model.
// MARK: - TableColumn
public protocol TableColumn: RawRepresentable, CaseIterable where RawValue == String {
var dataType: ColumnDataType { get }
}
// MARK: - RowValuetype
public protocol RowValueType {
init(_ cursor: CursorIterator) throws
}
public protocol Table {
associatedtype EntityType: RowValueType
associatedtype ColumnType: TableColumn
...
}
Below are examples of UserTable
and PetTable
that store user and pet information.
struct UserTable: Table {
// Define table column
enum Columns: String, TableColumn {
case uid
case name
case age
case email
case phone
case introduction = "intro"
// define each column data type
var dataType: ColumnDataType {
switch self {
case .uid: return .text([.primaryKey(autoIncrement: false)])
case .name: return .text([.notNull])
case .age: return .integer([])
case .email: return .text([.unique, .notNull])
case .phone: return .text([])
case .introduction: return .text([])
}
}
}
// Define Entity that conforms to RowValueType protocol.
struct Entiity: RowValueType {
let uid: String
let name: String
let age: Int?
let email: String
let phone: String?
let introduction: String?
init(_ cursor: CursorIterator) throws {
self.uid = try cursor.next().unwrap()
self.name = try cursor.next().unwrap()
self.age = cursor.next()
self.email = try cursor.next().unwrap()
self.phone = cursor.next()
self.introduction = cursor.next()
}
}
typealias EntityType = Entiity
typealias ColumnType = Columns
static var tableName: String { "Users" }
// mapping each column with Entity property
static func scalar(_ entity: Entiity, for column: Columns) -> ScalarType? {
switch column {
case .uid: return entity.uid
case .name: return entity.name
case .age: return entity.age
case .email: return entity.email
case .phone: return entity.phone
case .introduction: return entity.introduction
}
}
}
struct User {
let uid: String
let name: String
var age: Int?
let email: String
var phone: String?
var introduction: String?
init(dummy index: Int) {
self.uid = "uid:\(index)"
self.name = "name:\(index)"
self.age = index % 2 == 0 ? nil: index
self.email = "email:\(index)"
self.introduction = ["hello", "world", "!"].randomElement()
}
}
extension UserTable.Entiity {
init(_ user: User) {
self.uid = user.uid
self.name = user.name
self.age = user.age
self.email = user.email
self.phone = user.phone
self.introduction = user.introduction
}
}
// define pet table
struct PetTable: Table {
enum Columns: String, TableColumn {
case uid
case ownerID = "owner_id"
case name
var dataType: ColumnDataType {
switch self {
case .uid: return .text([.primaryKey(autoIncrement: false), .notNull])
case .ownerID: return .text([.notNull])
case .name: return .text([.notNull])
}
}
}
struct Entity: RowValueType {
let uid: String
let ownerID: String
let name: String
init(_ cursor: CursorIterator) throws {
self.uid = try cursor.next().unwrap()
self.ownerID = try cursor.next().unwrap()
self.name = try cursor.next().unwrap()
}
init(uid: String, ownerID: String, name: String) {
self.uid = uid
self.ownerID = ownerID
self.name = name
}
}
static var tableName: String { "pets" }
typealias ColumnType = Columns
typealias EntityType = Entity
static func scalar(_ entity: Entity, for column: Columns) -> ScalarType? {
switch column {
case .uid: return entity.uid
case .ownerID: return entity.ownerID
case .name: return entity.name
}
}
}
Another requirement for tables is to indicate which property of the entity matches each column using the static func scalar(_ entity: Entity, for column: Columns) -> ScalarType?
type method.
(Actual data is stored by matching entity property values according to the order of columns following the CaseIterable
protocol.)
data manipulation
Here are some basic data manipulation usages.
func testSaveDataUsaga() {
_ = self.service.open(path: self.dbPath)
let users = (0..<10).map{ User(dummy: $0) }
let entities = users.map{ UserTable.Entiity($0) }
/**
The task executed by the run method can be sequentially executed synchronously/asynchronously according to the request method by the serial queue.
*/
self.service.run(execute: { try $0.insert(UserTable.self, entities: entities) }) { result in
print("save users result: \(result)")
}
}
func testLoadDataUsage() {
let users = (0..<10).map{ User(dummy: $0) }
let entities = users.map{ UserTable.Entiity($0) }
let table = UserTable.self
_ = self.service.open(path: self.dbPath)
_ = self.service.run(execute: { try $0.insert(table, entities: entities) })
let query1 = table.selectAll{ $0.age > 5 }
let result1: Result<[UserTable.Entiity], Error> = self.service.run(execute: { try $0.load(query1) })
print("load users older than 5 result \(result1)")
let query2 = table.selectAll{ $0.age > 5 && $0.introduction == "hello" }
let result2: Result<[UserTable.Entiity], Error> = self.service.run(execute: { try $0.load(query2) })
print("load result: \(result2)")
let query3 = table.selectAll{ $0.uid == "uid:1" }
let result3: Result<UserTable.Entiity?, Error> = self.service.run(execute: { try $0.loadOne(query3) })
print("load user 1 result: \(result3)")
let query4 = table.selectAll()
let result4: Result<[UserTable.Entiity], Error> = self.service.run(execute: { try $0.load(query4) })
print("load all users result: \(result4)")
}
func testUpdateUsage() {
let table = UserTable.self
var oldUser = User(dummy: 1)
oldUser.introduction = "old_introduction"
_ = self.service.open(path: self.dbPath)
_ = self.service.run(execute: { try $0.insert(table, entities: [.init(oldUser)]) })
let updateQuery = table.update { [$0.introduction == "newIntro"]}
.where{ $0.uid == "uid:1" }
_ = self.service.run(execute: { try $0.update(table, query: updateQuery) })
let selectQuery = table.selectAll{ $0.uid == "uid:1" }
self.service.run(execute: { try $0.loadOne(selectQuery) }) { (result: Result<UserTable.Entiity?, Error>) in
guard case let .success(user) = result, let updatedUser = user else { return }
print("updated user intro: \(updatedUser.introduction)")
}
}
func testDeleteUsage() {
let table = UserTable.self
let users = (0..<10).map{ User(dummy: $0) }
let entities = users.map{ UserTable.Entiity($0) }
_ = self.service.open(path: self.dbPath)
_ = self.service.run(execute: { try $0.insert(table, entities: entities) })
let deleteQuery = table.delete().where{ $0.uid == "uid:1" }
self.service.run(execute: { try $0.delete(table, query: deleteQuery) }) { result in
print("delete result: \(result)")
}
self.wait(for: [expect], timeout: 0.01)
}
func testJoinQueryUsage() {
let users = (0..<10).map{ User(dummy: $0) }.map{ UserTable.Entiity($0) }
let pets: [PetTable.Entity] = [
.init(uid: "p0", ownerID: "uid:1", name: "foo"),
.init(uid: "p1", ownerID: "uid:3", name: "bar")
]
_ = self.service.open(path: self.dbPath)
_ = self.service.run(execute: { try $0.insert(UserTable.self, entities: users) })
_ = self.service.run(execute: { try $0.insert(PetTable.self, entities: pets) })
let allUserQuery = UserTable.selectAll()
let allpetsQuery = PetTable.selectAll()
let joinQuery = allUserQuery.innerJoin(with: allpetsQuery, on: { ($0.uid, $1.ownerID) })
typealias UserAndPetPair = (UserTable.Entiity, PetTable.Entity)
let mapping: (CursorIterator) throws -> UserAndPetPair? = { cursor in
return (try UserTable.Entiity(cursor), try PetTable.Entity(cursor))
}
let result = self.service.run(execute: { try $0.load(joinQuery, mapping: mapping) })
let petOwnerAndPets = try? result.get()
print("pet owner and pet: \(petOwnerAndPets)")
}
For more information on how to use it, see unit tests.
As you can see from the readme, this project has a lot of missing features and a lot of room for improvement. Feedback or contributions to the project are always welcome. ?