Refactor activities table

This commit is contained in:
Christoph Hagen
2024-02-02 14:12:57 +01:00
parent 03b4f84807
commit 8cf18f070f
5 changed files with 152 additions and 134 deletions

View File

@@ -0,0 +1,19 @@
import Foundation
import HealthKit
extension HKWorkoutActivity: Comparable {
public static func < (lhs: HKWorkoutActivity, rhs: HKWorkoutActivity) -> Bool {
lhs.startDate < rhs.startDate
}
}
extension HKWorkoutActivity {
var externalUUID: UUID? {
guard let string = metadata?[HKMetadataKeyExternalUUID] as? String else {
return nil
}
return UUID(uuidString: string)
}
}

View File

@@ -0,0 +1,119 @@
import Foundation
import SQLite
import HealthKit
struct WorkoutActivitiesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("workout_activities")
let rowId = Expression<Int>("ROWID")
let uuid = Expression<Data>("uuid")
let ownerId = Expression<Int>("owner_id")
let isPrimaryActivity = Expression<Bool>("is_primary_activity")
let activityType = Expression<Int>("activity_type")
let locationType = Expression<Int>("location_type")
let swimmingLocationType = Expression<Int>("swimming_location_type")
let lapLength = Expression<Data?>("lap_length")
let startDate = Expression<Double>("start_date")
let endDate = Expression<Double>("end_date")
let duration = Expression<Double>("duration")
let metadata = Expression<Data?>("metadata")
func activities() throws -> [HKWorkoutActivity] {
try database.prepare(table).map(activity)
}
func activity(from row: Row) throws -> HKWorkoutActivity {
let configuration = HKWorkoutConfiguration()
configuration.lapLength = try row[lapLength].map(WorkoutActivitiesTable.lapLength)
configuration.activityType = .init(rawValue: UInt(row[activityType]))!
configuration.locationType = .init(rawValue: row[locationType])!
configuration.swimmingLocationType = .init(rawValue: row[swimmingLocationType])!
let start = Date(timeIntervalSinceReferenceDate: row[startDate])
let end = Date(timeIntervalSinceReferenceDate: row[endDate])
let uuid = row[uuid].uuidString
var metadata: [String : Any] = [ : ]
metadata[HKMetadataKeyExternalUUID] = uuid
// duration: row[columnDuration]
// isPrimaryActivity: row[columnIsPrimaryActivity]
// metadata: row[columnMetadata]
// TODO: Decode activity metadata
return .init(
workoutConfiguration: configuration,
start: start,
end: end,
metadata: metadata)
}
func activities(for workoutId: Int) throws -> [HKWorkoutActivity] {
try database.prepare(table.filter(ownerId == workoutId)).map(activity)
}
func create(referencing workouts: WorkoutsTable) throws {
try database.execute("CREATE TABLE workout_activities (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE NOT NULL, owner_id INTEGER NOT NULL REFERENCES workouts(data_id) ON DELETE CASCADE, is_primary_activity INTEGER NOT NULL, activity_type INTEGER NOT NULL, location_type INTEGER NOT NULL, swimming_location_type INTEGER NOT NULL, lap_length BLOB, start_date REAL NOT NULL, end_date REAL NOT NULL, duration REAL NOT NULL, metadata BLOB)")
/*
try database.run(table.create { t in
t.column(rowId, primaryKey: .autoincrement)
t.column(uuid, unique: true)
t.column(ownerId, references: workouts.table, workouts.dataId) // TODO: ON DELETE CASCADE
t.column(isPrimaryActivity)
t.column(activityType)
t.column(locationType)
t.column(swimmingLocationType)
t.column(lapLength)
t.column(startDate)
t.column(endDate)
t.column(duration)
t.column(metadata)
})
*/
}
func insert(_ element: HKWorkoutActivity, isPrimaryActivity: Bool, dataId: Int) throws {
try database.run(table.insert(
uuid <- (element.externalUUID ?? element.uuid).uuidString.data(using: .utf8)!,
ownerId <- dataId,
self.isPrimaryActivity <- isPrimaryActivity, // Seems to always be 1
activityType <- Int(element.workoutConfiguration.activityType.rawValue),
locationType <- element.workoutConfiguration.locationType.rawValue,
swimmingLocationType <- element.workoutConfiguration.swimmingLocationType.rawValue,
lapLength <- try WorkoutActivitiesTable.lapLengthData(lapLength: element.workoutConfiguration.lapLength),
startDate <- element.startDate.timeIntervalSinceReferenceDate,
endDate <- element.endDate?.timeIntervalSinceReferenceDate ?? element.startDate.addingTimeInterval(element.duration).timeIntervalSinceReferenceDate,
duration <- element.duration,
metadata <- nil)
)
}
}
private extension WorkoutActivitiesTable {
static func lapLengthData(lapLength: HKQuantity?) throws -> Data? {
try lapLength.map { try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) }
}
static func lapLength(from data: Data) throws -> HKQuantity? {
try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data)
}
}

View File

@@ -8,9 +8,12 @@ struct WorkoutsTable {
let events: WorkoutEventsTable
let activities: WorkoutActivitiesTable
init(database: Connection) {
self.database = database
self.events = .init(database: database)
self.activities = .init(database: database)
}
let table = Table("workouts")
@@ -40,7 +43,7 @@ struct WorkoutsTable {
let id = row[dataId]
let events = try events.events(for: id, in: database)
let activities = try WorkoutActivityTable.activities(for: id, in: database)
let activities = try activities.activities(for: id)
let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys)
return .init(
id: id,
@@ -70,6 +73,7 @@ struct WorkoutsTable {
func createAll() throws {
try create()
try events.create(referencing: self)
try activities.create(referencing: self)
}
func insert(_ element: Workout) throws {
@@ -86,7 +90,7 @@ struct WorkoutsTable {
}
for activity in element.activities {
try WorkoutActivityTable.insert(activity, isPrimaryActivity: true, dataId: dataId, in: database)
try activities.insert(activity, isPrimaryActivity: true, dataId: dataId)
}
for (key, value) in element.metadata {

View File

@@ -1,128 +0,0 @@
import Foundation
import SQLite
import HealthKit
extension HKWorkoutActivity: Comparable {
public static func < (lhs: HKWorkoutActivity, rhs: HKWorkoutActivity) -> Bool {
lhs.startDate < rhs.startDate
}
}
enum WorkoutActivityTable {
private static let table = Table("workout_activities")
private static let columnId = Expression<Int>("ROWID")
private static let columnUUID = Expression<Data>("uuid")
private static let columnOwnerId = Expression<Int>("owner_id")
private static let columnIsPrimaryActivity = Expression<Bool>("is_primary_activity")
private static let columnActivityType = Expression<Int>("activity_type")
private static let columnLocationType = Expression<Int>("location_type")
private static let columnSwimmingLocationType = Expression<Int>("swimming_location_type")
private static let columnLapLength = Expression<Data?>("lap_length")
private static let columnStartDate = Expression<Double>("start_date")
private static let columnEndDate = Expression<Double>("end_date")
private static let columnDuration = Expression<Double>("duration")
private static let columnMetadata = Expression<Data?>("metadata")
private static func readAll(in database: Connection) throws -> [HKWorkoutActivity] {
try database.prepare(table).map(activity)
}
private static func activity(from row: Row) throws -> HKWorkoutActivity {
let configuration = HKWorkoutConfiguration()
configuration.lapLength = try row[columnLapLength].map(lapLength)
configuration.activityType = .init(rawValue: UInt(row[columnActivityType]))!
configuration.locationType = .init(rawValue: row[columnLocationType])!
configuration.swimmingLocationType = .init(rawValue: row[columnSwimmingLocationType])!
let start = Date(timeIntervalSinceReferenceDate: row[columnStartDate])
let end = Date(timeIntervalSinceReferenceDate: row[columnEndDate])
let uuid = row[columnUUID].uuidString
var metadata: [String : Any] = [ : ]
metadata[HKMetadataKeyExternalUUID] = uuid
// duration: row[columnDuration]
// isPrimaryActivity: row[columnIsPrimaryActivity]
// metadata: row[columnMetadata]
#warning("Decode metadata")
return .init(
workoutConfiguration: configuration,
start: start,
end: end,
metadata: metadata)
}
static func activities(for workoutId: Int, in database: Connection) throws -> [HKWorkoutActivity] {
try database.prepare(table.filter(columnOwnerId == workoutId)).map(activity)
}
static func create(in database: Connection) throws {
//try database.execute("CREATE TABLE workout_activities (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, uuid BLOB UNIQUE NOT NULL, owner_id INTEGER NOT NULL REFERENCES workouts(data_id) ON DELETE CASCADE, is_primary_activity INTEGER NOT NULL, activity_type INTEGER NOT NULL, location_type INTEGER NOT NULL, swimming_location_type INTEGER NOT NULL, lap_length BLOB, start_date REAL NOT NULL, end_date REAL NOT NULL, duration REAL NOT NULL, metadata BLOB)")
try database.run(table.create { t in
t.column(columnId, primaryKey: .autoincrement)
t.column(columnUUID)
t.column(columnOwnerId, references: Table("workouts"), Expression<Int>("data_id"))
t.column(columnIsPrimaryActivity)
t.column(columnActivityType)
t.column(columnLocationType)
t.column(columnSwimmingLocationType)
t.column(columnLapLength)
t.column(columnStartDate)
t.column(columnEndDate)
t.column(columnDuration)
t.column(columnMetadata)
})
}
static func insert(_ element: HKWorkoutActivity, isPrimaryActivity: Bool, dataId: Int, in database: Connection) throws {
try database.run(table.insert(
columnUUID <- (element.externalUUID ?? element.uuid).uuidString.data(using: .utf8)!,
columnOwnerId <- dataId,
columnIsPrimaryActivity <- isPrimaryActivity, // Seems to always be 1
columnActivityType <- Int(element.workoutConfiguration.activityType.rawValue),
columnLocationType <- element.workoutConfiguration.locationType.rawValue,
columnSwimmingLocationType <- element.workoutConfiguration.swimmingLocationType.rawValue,
columnLapLength <- try lapLengthData(lapLength: element.workoutConfiguration.lapLength),
columnStartDate <- element.startDate.timeIntervalSinceReferenceDate,
columnEndDate <- element.endDate?.timeIntervalSinceReferenceDate ?? element.startDate.addingTimeInterval(element.duration).timeIntervalSinceReferenceDate,
columnDuration <- element.duration)
//columnMetadata <- element.metadata)
)
}
}
private extension HKWorkoutActivity {
var externalUUID: UUID? {
guard let string = metadata?[HKMetadataKeyExternalUUID] as? String else {
return nil
}
return UUID(uuidString: string)
}
}
private extension WorkoutActivityTable {
static func lapLengthData(lapLength: HKQuantity?) throws -> Data? {
try lapLength.map { try NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: false) }
}
static func lapLength(from data: Data) throws -> HKQuantity? {
try NSKeyedUnarchiver.unarchivedObject(ofClass: HKQuantity.self, from: data)
}
}