SQLite.swift
SQLite.swift is developed as "A type-safe, Swift-language layer over SQLite3" - a Swift-exclusive SQLite database access library. It functions as a thin wrapper over SQLite3, providing database operations leveraging Swift's type safety and expressive syntax. Supporting all Apple platforms including iOS, macOS, watchOS, and tvOS, it is widely adopted in mobile app development and Mac application development as a lightweight alternative to heavyweight frameworks like Core Data and Realm.
GitHub Overview
stephencelis/SQLite.swift
A type-safe, Swift-language layer over SQLite3.
Topics
Star History
Library
SQLite.swift
Overview
SQLite.swift is developed as "A type-safe, Swift-language layer over SQLite3" - a Swift-exclusive SQLite database access library. It functions as a thin wrapper over SQLite3, providing database operations leveraging Swift's type safety and expressive syntax. Supporting all Apple platforms including iOS, macOS, watchOS, and tvOS, it is widely adopted in mobile app development and Mac application development as a lightweight alternative to heavyweight frameworks like Core Data and Realm.
Details
SQLite.swift 2025 edition leverages the latest Swift 5.9+ features (async/await, Property Wrappers, Result Builder, etc.) and is optimized for modern Swift development paradigms. It comprehensively supports type-safe query builders, prepared statements, transaction management, and migration functionality, providing the reliability and performance necessary for enterprise-level iOS app development. Natural integration with SwiftUI, Combine support, and complete Swift Package Manager compatibility enable seamless integration into modern iOS development workflows.
Key Features
- Type Safety: Completely type-safe query construction leveraging Swift's type system
- Lightweight Design: Minimal overhead as a thin wrapper over SQLite3
- Full Platform Support: iOS, macOS, watchOS, tvOS support
- Modern Swift: Support for latest features like async/await, Property Wrappers
- Query Builder: Intuitive and readable query construction API
- Migration: Automatic management of database schema changes
Pros and Cons
Pros
- Retains SQLite3's high performance while enjoying Swift's type safety
- Lightweight with lower learning costs compared to Core Data or Realm
- High transparency in database operations with SQL-like descriptions
- Consistent development experience across all Apple platforms
- Easy introduction and version management through Swift Package Manager
- High security through prepared statements
Cons
- Swift/Apple platform exclusive with no cross-platform support
- No advanced object mapping features like Core Data
- Complex relationship management requires manual implementation
- Performance optimization for large datasets depends on developer
- Background processing and multi-threading require careful attention
- NoSQL-like flexibility depends on SQLite constraints
Reference Pages
Code Examples
Setup
// Package.swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "SQLiteSwiftExample",
platforms: [
.iOS(.v15),
.macOS(.v12),
.watchOS(.v8),
.tvOS(.v15)
],
dependencies: [
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3")
],
targets: [
.target(
name: "SQLiteSwiftExample",
dependencies: [
.product(name: "SQLite", package: "SQLite.swift")
]
),
.testTarget(
name: "SQLiteSwiftExampleTests",
dependencies: ["SQLiteSwiftExample"]
)
]
)
// For iOS Xcode projects
// File -> Add Package Dependencies -> https://github.com/stephencelis/SQLite.swift.git
import SQLite
import Foundation
Basic Usage
import SQLite
import Foundation
// Database connection and table definition
class UserDatabase {
private var db: Connection?
// Table definition
private let users = Table("users")
private let id = Expression<Int64>("id")
private let name = Expression<String>("name")
private let email = Expression<String>("email")
private let age = Expression<Int>("age")
private let isActive = Expression<Bool>("is_active")
private let createdAt = Expression<Date>("created_at")
private let profile = Expression<String?>("profile") // Store as JSON string
// Posts table
private let posts = Table("posts")
private let postId = Expression<Int64>("post_id")
private let userId = Expression<Int64>("user_id")
private let title = Expression<String>("title")
private let content = Expression<String>("content")
private let published = Expression<Bool>("published")
private let publishedAt = Expression<Date?>("published_at")
private let viewCount = Expression<Int>("view_count")
init() {
do {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let dbPath = "\(path)/userdb.sqlite3"
db = try Connection(dbPath)
print("Database connected at: \(dbPath)")
createTables()
} catch {
print("Database connection failed: \(error)")
}
}
// Create tables
private func createTables() {
do {
// Create users table
try db?.run(users.create(ifNotExists: true) { table in
table.column(id, primaryKey: .autoincrement)
table.column(name)
table.column(email, unique: true)
table.column(age)
table.column(isActive, defaultValue: true)
table.column(createdAt, defaultValue: Date())
table.column(profile)
})
// Create posts table
try db?.run(posts.create(ifNotExists: true) { table in
table.column(postId, primaryKey: .autoincrement)
table.column(userId)
table.column(title)
table.column(content)
table.column(published, defaultValue: false)
table.column(publishedAt)
table.column(viewCount, defaultValue: 0)
table.foreignKey(userId, references: users, id, delete: .cascade)
})
// Create indexes
try db?.run(users.createIndex(email, ifNotExists: true))
try db?.run(posts.createIndex(userId, ifNotExists: true))
try db?.run(posts.createIndex(published, ifNotExists: true))
print("Tables created successfully")
} catch {
print("Create table error: \(error)")
}
}
}
// User model
struct User {
let id: Int64?
let name: String
let email: String
let age: Int
let isActive: Bool
let createdAt: Date
let profile: UserProfile?
init(id: Int64? = nil, name: String, email: String, age: Int, isActive: Bool = true, createdAt: Date = Date(), profile: UserProfile? = nil) {
self.id = id
self.name = name
self.email = email
self.age = age
self.isActive = isActive
self.createdAt = createdAt
self.profile = profile
}
}
// User profile (JSON)
struct UserProfile: Codable {
let bio: String?
let website: String?
let location: String?
let skills: [String]
init(bio: String? = nil, website: String? = nil, location: String? = nil, skills: [String] = []) {
self.bio = bio
self.website = website
self.location = location
self.skills = skills
}
}
// Post model
struct Post {
let id: Int64?
let userId: Int64
let title: String
let content: String
let published: Bool
let publishedAt: Date?
let viewCount: Int
init(id: Int64? = nil, userId: Int64, title: String, content: String, published: Bool = false, publishedAt: Date? = nil, viewCount: Int = 0) {
self.id = id
self.userId = userId
self.title = title
self.content = content
self.published = published
self.publishedAt = publishedAt
self.viewCount = viewCount
}
}
// User service extension
extension UserDatabase {
// Create user
func createUser(_ user: User) throws -> Int64 {
guard let db = db else { throw DatabaseError.connectionFailed }
let profileJSON = try user.profile?.toJSONString()
let insert = users.insert(
name <- user.name,
email <- user.email,
age <- user.age,
isActive <- user.isActive,
createdAt <- user.createdAt,
profile <- profileJSON
)
let rowid = try db.run(insert)
print("Created user with ID: \(rowid)")
return rowid
}
// Get all users
func getAllUsers() throws -> [User] {
guard let db = db else { throw DatabaseError.connectionFailed }
var result: [User] = []
for user in try db.prepare(users.order(createdAt.desc)) {
let userProfile = try? UserProfile.fromJSONString(user[profile])
result.append(User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
))
}
return result
}
// Get user by ID
func getUserById(_ userId: Int64) throws -> User? {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(id == userId)
guard let user = try db.pluck(query) else { return nil }
let userProfile = try? UserProfile.fromJSONString(user[profile])
return User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
)
}
// Search user by email
func getUserByEmail(_ userEmail: String) throws -> User? {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(email == userEmail)
guard let user = try db.pluck(query) else { return nil }
let userProfile = try? UserProfile.fromJSONString(user[profile])
return User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
)
}
// Update user
func updateUser(_ userId: Int64, name: String, email: String, age: Int, profile: UserProfile?) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let userToUpdate = users.filter(id == userId)
let profileJSON = try profile?.toJSONString()
let update = userToUpdate.update(
self.name <- name,
self.email <- email,
self.age <- age,
self.profile <- profileJSON
)
let changes = try db.run(update)
return changes > 0
}
// Change user active status
func setUserActive(_ userId: Int64, active: Bool) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let userToUpdate = users.filter(id == userId)
let update = userToUpdate.update(isActive <- active)
let changes = try db.run(update)
return changes > 0
}
// Delete user
func deleteUser(_ userId: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let userToDelete = users.filter(id == userId)
let changes = try db.run(userToDelete.delete())
return changes > 0
}
// Search by name
func searchUsersByName(_ searchTerm: String) throws -> [User] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(name.like("%\(searchTerm)%")).order(name)
var result: [User] = []
for user in try db.prepare(query) {
let userProfile = try? UserProfile.fromJSONString(user[profile])
result.append(User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
))
}
return result
}
// Search by age range
func getUsersByAgeRange(min: Int, max: Int) throws -> [User] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = users.filter(age >= min && age <= max).order(age, name)
var result: [User] = []
for user in try db.prepare(query) {
let userProfile = try? UserProfile.fromJSONString(user[profile])
result.append(User(
id: user[id],
name: user[name],
email: user[email],
age: user[age],
isActive: user[isActive],
createdAt: user[createdAt],
profile: userProfile
))
}
return result
}
// Count active users
func getActiveUserCount() throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
return try db.scalar(users.filter(isActive == true).count)
}
// Total user count
func getTotalUserCount() throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
return try db.scalar(users.count)
}
}
// Usage example
func demonstrateBasicOperations() {
let database = UserDatabase()
do {
// Create users
let profile1 = UserProfile(bio: "iOS Developer", website: "https://example.com", location: "Tokyo", skills: ["Swift", "iOS", "SwiftUI"])
let user1 = User(name: "Alice Johnson", email: "[email protected]", age: 28, profile: profile1)
let userId1 = try database.createUser(user1)
let profile2 = UserProfile(bio: "Backend Developer", location: "Osaka", skills: ["Swift", "Vapor", "PostgreSQL"])
let user2 = User(name: "Bob Smith", email: "[email protected]", age: 32, profile: profile2)
let userId2 = try database.createUser(user2)
let user3 = User(name: "Charlie Brown", email: "[email protected]", age: 25)
let userId3 = try database.createUser(user3)
print("Created users: \(userId1), \(userId2), \(userId3)")
// Get all users
let allUsers = try database.getAllUsers()
print("\n=== All Users ===")
for user in allUsers {
print("ID: \(user.id ?? 0), Name: \(user.name), Email: \(user.email), Age: \(user.age)")
if let profile = user.profile {
print(" Bio: \(profile.bio ?? "N/A"), Skills: \(profile.skills.joined(separator: ", "))")
}
}
// Get specific user
if let retrievedUser = try database.getUserById(userId1) {
print("\n=== Retrieved User ===")
print("Retrieved: \(retrievedUser.name) (\(retrievedUser.email))")
}
// Update user
let updatedProfile = UserProfile(bio: "Senior iOS Developer", website: "https://alice-dev.com", location: "Tokyo", skills: ["Swift", "iOS", "SwiftUI", "Combine"])
let updated = try database.updateUser(userId1, name: "Alice Johnson Smith", email: "[email protected]", age: 29, profile: updatedProfile)
if updated {
print("\n=== User Updated ===")
if let updatedUser = try database.getUserById(userId1) {
print("Updated: \(updatedUser.name) (\(updatedUser.email))")
}
}
// Name search
let searchResults = try database.searchUsersByName("Alice")
print("\n=== Search Results ===")
for user in searchResults {
print("Found: \(user.name) (\(user.email))")
}
// Age range search
let ageRangeUsers = try database.getUsersByAgeRange(min: 25, max: 30)
print("\n=== Users aged 25-30 ===")
for user in ageRangeUsers {
print("\(user.name): \(user.age) years old")
}
// Statistics
let totalUsers = try database.getTotalUserCount()
let activeUsers = try database.getActiveUserCount()
print("\n=== Statistics ===")
print("Total users: \(totalUsers)")
print("Active users: \(activeUsers)")
} catch {
print("Error: \(error)")
}
}
Post Functionality and JOIN Operations
// Post service extension
extension UserDatabase {
// Create post
func createPost(_ post: Post) throws -> Int64 {
guard let db = db else { throw DatabaseError.connectionFailed }
let insert = posts.insert(
userId <- post.userId,
title <- post.title,
content <- post.content,
published <- post.published,
publishedAt <- post.publishedAt,
viewCount <- post.viewCount
)
let rowid = try db.run(insert)
print("Created post with ID: \(rowid)")
return rowid
}
// Get post
func getPostById(_ postIdValue: Int64) throws -> Post? {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts.filter(postId == postIdValue)
guard let post = try db.pluck(query) else { return nil }
return Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
)
}
// Get user's posts
func getPostsByUser(_ userIdValue: Int64) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts.filter(userId == userIdValue).order(postId.desc)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// Get published posts
func getPublishedPosts(limit: Int = 20, offset: Int = 0) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts
.filter(published == true)
.order(publishedAt.desc)
.limit(limit, offset: offset)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// Get posts with user information joined
func getPostsWithAuthors(limit: Int = 20) throws -> [(Post, User)] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts
.join(users, on: users[id] == posts[userId])
.order(posts[postId].desc)
.limit(limit)
var result: [(Post, User)] = []
for row in try db.prepare(query) {
let post = Post(
id: row[posts[postId]],
userId: row[posts[userId]],
title: row[posts[title]],
content: row[posts[content]],
published: row[posts[published]],
publishedAt: row[posts[publishedAt]],
viewCount: row[posts[viewCount]]
)
let userProfile = try? UserProfile.fromJSONString(row[users[profile]])
let user = User(
id: row[users[id]],
name: row[users[name]],
email: row[users[email]],
age: row[users[age]],
isActive: row[users[isActive]],
createdAt: row[users[createdAt]],
profile: userProfile
)
result.append((post, user))
}
return result
}
// Publish post
func publishPost(_ postIdValue: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let postToUpdate = posts.filter(postId == postIdValue)
let update = postToUpdate.update(
published <- true,
publishedAt <- Date()
)
let changes = try db.run(update)
return changes > 0
}
// Increment view count
func incrementViewCount(_ postIdValue: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let postToUpdate = posts.filter(postId == postIdValue)
let update = postToUpdate.update(viewCount <- viewCount + 1)
let changes = try db.run(update)
return changes > 0
}
// Delete post
func deletePost(_ postIdValue: Int64) throws -> Bool {
guard let db = db else { throw DatabaseError.connectionFailed }
let postToDelete = posts.filter(postId == postIdValue)
let changes = try db.run(postToDelete.delete())
return changes > 0
}
// Search posts by title
func searchPostsByTitle(_ searchTerm: String) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts.filter(title.like("%\(searchTerm)%")).order(postId.desc)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// Get popular posts (by view count)
func getPopularPosts(limit: Int = 10) throws -> [Post] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = posts
.filter(published == true)
.order(viewCount.desc)
.limit(limit)
var result: [Post] = []
for post in try db.prepare(query) {
result.append(Post(
id: post[postId],
userId: post[userId],
title: post[title],
content: post[content],
published: post[published],
publishedAt: post[publishedAt],
viewCount: post[viewCount]
))
}
return result
}
// User post counts
func getUserPostCounts() throws -> [(User, Int)] {
guard let db = db else { throw DatabaseError.connectionFailed }
let query = """
SELECT u.*, COUNT(p.post_id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
ORDER BY post_count DESC
"""
var result: [(User, Int)] = []
for row in try db.prepare(query) {
let userProfile = try? UserProfile.fromJSONString(row[6] as? String)
let user = User(
id: row[0] as! Int64,
name: row[1] as! String,
email: row[2] as! String,
age: row[3] as! Int,
isActive: row[4] as! Bool,
createdAt: row[5] as! Date,
profile: userProfile
)
let postCount = row[7] as! Int
result.append((user, postCount))
}
return result
}
// Post statistics
func getPostStatistics() throws -> (total: Int, published: Int, avgViewCount: Double) {
guard let db = db else { throw DatabaseError.connectionFailed }
let totalPosts = try db.scalar(posts.count)
let publishedPosts = try db.scalar(posts.filter(published == true).count)
let avgViews = try db.scalar(posts.select(viewCount.average)) ?? 0.0
return (total: totalPosts, published: publishedPosts, avgViewCount: avgViews)
}
}
// Usage example
func demonstratePostOperations() {
let database = UserDatabase()
do {
// Create user
let user = User(name: "John Doe", email: "[email protected]", age: 30)
let userId = try database.createUser(user)
// Create posts
let post1 = Post(userId: userId, title: "My First Post", content: "This is the content of my first post.")
let post2 = Post(userId: userId, title: "Another Post", content: "This is another interesting post.")
let post3 = Post(userId: userId, title: "Published Post", content: "This post is published.", published: true, publishedAt: Date())
let postId1 = try database.createPost(post1)
let postId2 = try database.createPost(post2)
let postId3 = try database.createPost(post3)
print("Created posts: \(postId1), \(postId2), \(postId3)")
// Publish post
_ = try database.publishPost(postId1)
print("Published post: \(postId1)")
// Increment view count
_ = try database.incrementViewCount(postId1)
_ = try database.incrementViewCount(postId3)
_ = try database.incrementViewCount(postId3)
// Get published posts
let publishedPosts = try database.getPublishedPosts()
print("\n=== Published Posts ===")
for post in publishedPosts {
print("Post: \(post.title) (Views: \(post.viewCount))")
}
// Join posts and user information
let postsWithAuthors = try database.getPostsWithAuthors()
print("\n=== Posts with Authors ===")
for (post, author) in postsWithAuthors {
print("Post: \(post.title) by \(author.name)")
}
// Popular posts
let popularPosts = try database.getPopularPosts(limit: 5)
print("\n=== Popular Posts ===")
for post in popularPosts {
print("Popular: \(post.title) (\(post.viewCount) views)")
}
// Statistics
let stats = try database.getPostStatistics()
print("\n=== Post Statistics ===")
print("Total posts: \(stats.total)")
print("Published posts: \(stats.published)")
print("Average view count: \(String(format: "%.1f", stats.avgViewCount))")
// User post counts
let userPostCounts = try database.getUserPostCounts()
print("\n=== User Post Counts ===")
for (user, count) in userPostCounts {
print("\(user.name): \(count) posts")
}
} catch {
print("Error: \(error)")
}
}
Transactions and Advanced Features
// Transaction functionality and batch processing
extension UserDatabase {
// Transaction: Create user and initial posts simultaneously
func createUserWithPosts(user: User, initialPosts: [Post]) throws -> (userId: Int64, postIds: [Int64]) {
guard let db = db else { throw DatabaseError.connectionFailed }
var userId: Int64 = 0
var postIds: [Int64] = []
try db.transaction {
// Create user
let profileJSON = try user.profile?.toJSONString()
let userInsert = users.insert(
name <- user.name,
email <- user.email,
age <- user.age,
isActive <- user.isActive,
createdAt <- user.createdAt,
profile <- profileJSON
)
userId = try db.run(userInsert)
// Create initial posts
for postData in initialPosts {
let postInsert = posts.insert(
self.userId <- userId,
title <- postData.title,
content <- postData.content,
published <- postData.published,
publishedAt <- postData.publishedAt,
viewCount <- postData.viewCount
)
let postId = try db.run(postInsert)
postIds.append(postId)
}
}
return (userId: userId, postIds: postIds)
}
// Transaction: Transfer posts between users
func transferPosts(fromUserId: Int64, toUserId: Int64) throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
var transferCount = 0
try db.transaction {
// Check existence of both users
guard try db.pluck(users.filter(id == fromUserId)) != nil else {
throw DatabaseError.userNotFound(fromUserId)
}
guard try db.pluck(users.filter(id == toUserId)) != nil else {
throw DatabaseError.userNotFound(toUserId)
}
// Transfer posts
let postsToTransfer = posts.filter(userId == fromUserId)
transferCount = try db.run(postsToTransfer.update(userId <- toUserId))
}
return transferCount
}
// Batch processing: Bulk update multiple users' ages
func batchUpdateUserAges(userAgeUpdates: [(userId: Int64, newAge: Int)]) throws -> Int {
guard let db = db else { throw DatabaseError.connectionFailed }
var totalUpdated = 0
try db.transaction {
for update in userAgeUpdates {
let userToUpdate = users.filter(id == update.userId)
let changes = try db.run(userToUpdate.update(age <- update.newAge))
totalUpdated += changes
}
}
return totalUpdated
}
// Conditional deletion: Delete inactive users and their posts
func cleanupInactiveUsers() throws -> (deletedUsers: Int, deletedPosts: Int) {
guard let db = db else { throw DatabaseError.connectionFailed }
var deletedUsers = 0
var deletedPosts = 0
try db.transaction {
// Get inactive users
let inactiveUsers = users.filter(isActive == false)
// Get inactive user IDs
var inactiveUserIds: [Int64] = []
for user in try db.prepare(inactiveUsers) {
inactiveUserIds.append(user[id])
}
// Delete related posts
for userIdValue in inactiveUserIds {
let userPosts = posts.filter(userId == userIdValue)
deletedPosts += try db.run(userPosts.delete())
}
// Delete users
deletedUsers = try db.run(inactiveUsers.delete())
}
return (deletedUsers: deletedUsers, deletedPosts: deletedPosts)
}
// Database backup (simple version)
func exportUserData() throws -> String {
guard let db = db else { throw DatabaseError.connectionFailed }
var exportData: [String: Any] = [:]
// Export user data
let allUsers = try getAllUsers()
let usersData = allUsers.map { user in
[
"id": user.id ?? 0,
"name": user.name,
"email": user.email,
"age": user.age,
"isActive": user.isActive,
"createdAt": ISO8601DateFormatter().string(from: user.createdAt),
"profile": user.profile?.toDictionary() ?? [:]
]
}
exportData["users"] = usersData
// Export post data
let allPosts = try db.prepare(posts)
let postsData = try allPosts.map { post in
[
"id": post[postId],
"userId": post[userId],
"title": post[title],
"content": post[content],
"published": post[published],
"publishedAt": post[publishedAt] != nil ? ISO8601DateFormatter().string(from: post[publishedAt]!) : nil,
"viewCount": post[viewCount]
]
}
exportData["posts"] = postsData
let jsonData = try JSONSerialization.data(withJSONObject: exportData, options: .prettyPrinted)
return String(data: jsonData, encoding: .utf8) ?? ""
}
// Database statistics
func getDatabaseStatistics() throws -> DatabaseStatistics {
guard let db = db else { throw DatabaseError.connectionFailed }
let totalUsers = try db.scalar(users.count)
let activeUsers = try db.scalar(users.filter(isActive == true).count)
let totalPosts = try db.scalar(posts.count)
let publishedPosts = try db.scalar(posts.filter(published == true).count)
let avgAge = try db.scalar(users.select(age.average)) ?? 0.0
let avgViewCount = try db.scalar(posts.select(viewCount.average)) ?? 0.0
return DatabaseStatistics(
totalUsers: totalUsers,
activeUsers: activeUsers,
totalPosts: totalPosts,
publishedPosts: publishedPosts,
averageUserAge: avgAge,
averagePostViewCount: avgViewCount
)
}
}
// Statistics structure
struct DatabaseStatistics {
let totalUsers: Int
let activeUsers: Int
let totalPosts: Int
let publishedPosts: Int
let averageUserAge: Double
let averagePostViewCount: Double
}
// Usage example
func demonstrateAdvancedFeatures() {
let database = UserDatabase()
do {
// Create user and initial posts simultaneously
let profile = UserProfile(bio: "Content Creator", skills: ["Writing", "Photography"])
let user = User(name: "Content Creator", email: "[email protected]", age: 25, profile: profile)
let initialPosts = [
Post(userId: 0, title: "Welcome Post", content: "Welcome to my blog!"),
Post(userId: 0, title: "About Me", content: "Let me introduce myself..."),
Post(userId: 0, title: "My First Tutorial", content: "This is my first technical post.", published: true, publishedAt: Date())
]
let (userId, postIds) = try database.createUserWithPosts(user: user, initialPosts: initialPosts)
print("Created user \(userId) with posts: \(postIds)")
// Create another user
let targetUser = User(name: "Another User", email: "[email protected]", age: 30)
let targetUserId = try database.createUser(targetUser)
// Transfer posts
let transferCount = try database.transferPosts(fromUserId: userId, toUserId: targetUserId)
print("Transferred \(transferCount) posts from user \(userId) to user \(targetUserId)")
// Batch age update
let ageUpdates = [
(userId: userId, newAge: 26),
(userId: targetUserId, newAge: 31)
]
let updatedCount = try database.batchUpdateUserAges(userAgeUpdates: ageUpdates)
print("Updated ages for \(updatedCount) users")
// Set user inactive
_ = try database.setUserActive(userId, active: false)
// Cleanup inactive users
let (deletedUsers, deletedPosts) = try database.cleanupInactiveUsers()
print("Cleanup: deleted \(deletedUsers) users and \(deletedPosts) posts")
// Statistics
let stats = try database.getDatabaseStatistics()
print("\n=== Database Statistics ===")
print("Total users: \(stats.totalUsers)")
print("Active users: \(stats.activeUsers)")
print("Total posts: \(stats.totalPosts)")
print("Published posts: \(stats.publishedPosts)")
print("Average user age: \(String(format: "%.1f", stats.averageUserAge))")
print("Average post view count: \(String(format: "%.1f", stats.averagePostViewCount))")
// Data export
let exportedData = try database.exportUserData()
print("\n=== Exported Data (first 200 chars) ===")
print(String(exportedData.prefix(200)) + "...")
} catch {
print("Error: \(error)")
}
}
Error Handling and Utility
// Custom error type
enum DatabaseError: Error, LocalizedError {
case connectionFailed
case userNotFound(Int64)
case postNotFound(Int64)
case invalidData(String)
case transactionFailed(String)
var errorDescription: String? {
switch self {
case .connectionFailed:
return "Database connection failed"
case .userNotFound(let id):
return "User not found: ID \(id)"
case .postNotFound(let id):
return "Post not found: ID \(id)"
case .invalidData(let message):
return "Invalid data: \(message)"
case .transactionFailed(let message):
return "Transaction failed: \(message)"
}
}
}
// UserProfile utility extension
extension UserProfile {
func toJSONString() throws -> String {
let encoder = JSONEncoder()
let data = try encoder.encode(self)
return String(data: data, encoding: .utf8) ?? ""
}
static func fromJSONString(_ jsonString: String?) throws -> UserProfile? {
guard let jsonString = jsonString,
let data = jsonString.data(using: .utf8) else {
return nil
}
let decoder = JSONDecoder()
return try decoder.decode(UserProfile.self, from: data)
}
func toDictionary() -> [String: Any] {
return [
"bio": bio ?? "",
"website": website ?? "",
"location": location ?? "",
"skills": skills
]
}
}
// Migration functionality
extension UserDatabase {
// Version management table
private var migrationTable: Table { Table("migrations") }
private var versionColumn: Expression<Int> { Expression<Int>("version") }
private var appliedAtColumn: Expression<Date> { Expression<Date>("applied_at") }
// Run migrations
func runMigrations() throws {
guard let db = db else { throw DatabaseError.connectionFailed }
// Create migration table
try db.run(migrationTable.create(ifNotExists: true) { table in
table.column(versionColumn, primaryKey: true)
table.column(appliedAtColumn, defaultValue: Date())
})
let currentVersion = getCurrentMigrationVersion()
// Run each migration sequentially
for version in (currentVersion + 1)...getLatestMigrationVersion() {
try runMigration(version: version)
try recordMigration(version: version)
print("Applied migration version \(version)")
}
}
private func getCurrentMigrationVersion() -> Int {
do {
guard let db = db else { return 0 }
if let latestMigration = try db.pluck(migrationTable.order(versionColumn.desc)) {
return latestMigration[versionColumn]
}
} catch {
// Migration table doesn't exist
}
return 0
}
private func getLatestMigrationVersion() -> Int {
return 3 // Current latest version
}
private func runMigration(version: Int) throws {
guard let db = db else { throw DatabaseError.connectionFailed }
switch version {
case 1:
// Add last_login_at column to users table
try db.run("ALTER TABLE users ADD COLUMN last_login_at DATETIME")
case 2:
// Add category column to posts table
try db.run("ALTER TABLE posts ADD COLUMN category TEXT DEFAULT 'general'")
case 3:
// Add avatar_url column to users table
try db.run("ALTER TABLE users ADD COLUMN avatar_url TEXT")
default:
throw DatabaseError.invalidData("Unknown migration version: \(version)")
}
}
private func recordMigration(version: Int) throws {
guard let db = db else { throw DatabaseError.connectionFailed }
let insert = migrationTable.insert(
versionColumn <- version,
appliedAtColumn <- Date()
)
try db.run(insert)
}
}
// Async support (iOS 15+)
@available(iOS 15.0, *)
extension UserDatabase {
func createUserAsync(_ user: User) async throws -> Int64 {
return try await withUnsafeThrowingContinuation { continuation in
do {
let userId = try createUser(user)
continuation.resume(returning: userId)
} catch {
continuation.resume(throwing: error)
}
}
}
func getAllUsersAsync() async throws -> [User] {
return try await withUnsafeThrowingContinuation { continuation in
do {
let users = try getAllUsers()
continuation.resume(returning: users)
} catch {
continuation.resume(throwing: error)
}
}
}
}
// Main execution example
func main() {
print("=== SQLite.swift Demo ===\n")
demonstrateBasicOperations()
print("\n" + String(repeating: "=", count: 50) + "\n")
demonstratePostOperations()
print("\n" + String(repeating: "=", count: 50) + "\n")
demonstrateAdvancedFeatures()
}
// Execute
main()