Refactor location samples table

This commit is contained in:
Christoph Hagen 2024-02-02 14:21:20 +01:00
parent 8cf18f070f
commit 1b6b89f053
5 changed files with 128 additions and 98 deletions

View File

@ -41,7 +41,7 @@
E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7C2B62930E005B83D3 /* SamplesTable.swift */; };
E201EC7F2B629B4C005B83D3 /* SampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7E2B629B4C005B83D3 /* SampleListView.swift */; };
E201EC812B631708005B83D3 /* Goal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC802B631708005B83D3 /* Goal.swift */; };
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSample.swift */; };
E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */; };
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */; };
E27BC6802B5E74D7003A8873 /* LocationSampleListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */; };
E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */; };
@ -66,6 +66,7 @@
E2FDFF272B6C56C70080A7B3 /* DataProvenancesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */; };
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */; };
E2FDFF2B2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */; };
E2FDFF2D2B6D23670080A7B3 /* DataSeriesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -100,7 +101,7 @@
E201EC7C2B62930E005B83D3 /* SamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTable.swift; sourceTree = "<group>"; };
E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = "<group>"; };
E201EC802B631708005B83D3 /* Goal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Goal.swift; sourceTree = "<group>"; };
E27BC6792B5D99AC003A8873 /* LocationSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSample.swift; sourceTree = "<group>"; };
E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSeriesDataTable.swift; sourceTree = "<group>"; };
E27BC67D2B5E6CE3003A8873 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = "<group>"; };
E27BC67F2B5E74D7003A8873 /* LocationSampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleListView.swift; sourceTree = "<group>"; };
E27BC6812B5E762D003A8873 /* LocationSampleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSampleDetailView.swift; sourceTree = "<group>"; };
@ -123,6 +124,7 @@
E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvenancesTable.swift; sourceTree = "<group>"; };
E2FDFF282B6D10D60080A7B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutActivity+Comparable.swift"; sourceTree = "<group>"; };
E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSeriesTable.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -210,7 +212,6 @@
E2FDFF232B6C509D0080A7B3 /* Tables */,
E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */,
E201EC802B631708005B83D3 /* Goal.swift */,
E27BC6792B5D99AC003A8873 /* LocationSample.swift */,
E201EC7A2B6275CA005B83D3 /* Metadata.swift */,
E201EC762B626FC1005B83D3 /* MetadataKey.swift */,
E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */,
@ -254,6 +255,7 @@
E2FDFF232B6C509D0080A7B3 /* Tables */ = {
isa = PBXGroup;
children = (
E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */,
E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */,
E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */,
E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */,
@ -262,6 +264,7 @@
E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */,
E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */,
8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */,
E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */,
);
path = Tables;
sourceTree = "<group>";
@ -386,13 +389,14 @@
E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */,
885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */,
E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */,
E27BC67A2B5D99AC003A8873 /* LocationSample.swift in Sources */,
E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */,
885002952B5D147100E7D4DB /* DetailRow.swift in Sources */,
E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */,
E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */,
E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */,
E27BC68A2B5FC255003A8873 /* UnitStringsTable.swift in Sources */,
8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */,
E2FDFF2D2B6D23670080A7B3 /* DataSeriesTable.swift in Sources */,
E27BC6862B5FBF0B003A8873 /* Sample.swift in Sources */,
E27BC67E2B5E6CE3003A8873 /* Sequence+Extensions.swift in Sources */,
E27BC68C2B5FC842003A8873 /* ActivitySamplesView.swift in Sources */,

View File

@ -15,6 +15,10 @@ final class HealthDatabase: ObservableObject {
private let workoutsTable: WorkoutsTable
private let locationSamples: LocationSeriesDataTable
private let dataSeries: DataSeriesTable
@Published
var workouts: [Workout] = []
@ -28,6 +32,8 @@ final class HealthDatabase: ObservableObject {
self.database = database
self.samples = .init(database: database)
self.workoutsTable = .init(database: database)
self.locationSamples = .init(database: database)
self.dataSeries = .init(database: database)
DispatchQueue.global().async {
self.readAllWorkouts()
@ -48,11 +54,11 @@ final class HealthDatabase: ObservableObject {
}
func locationSamples(for activity: HKWorkoutActivity) throws -> [LocationSample] {
try LocationSample.locationSamples(from: activity.startDate, to: activity.currentEndDate, in: database)
try locationSamples.locationSamples(from: activity.startDate, to: activity.currentEndDate)
}
func locationSampleCount(for activity: HKWorkoutActivity) throws -> Int {
try LocationSample.locationSampleCount(from: activity.startDate, to: activity.currentEndDate, in: database)
try locationSamples.locationSampleCount(from: activity.startDate, to: activity.currentEndDate)
}
func samples(for activity: HKWorkoutActivity) throws -> [Sample.DataType : [Sample]] {
@ -103,6 +109,7 @@ final class HealthDatabase: ObservableObject {
func createTables() throws {
try samples.createAll()
try workoutsTable.createAll()
try locationSamples.create(references: dataSeries)
}
}

View File

@ -1,92 +0,0 @@
import Foundation
import SQLite
import CoreLocation
typealias LocationSample = CLLocation
extension LocationSample {
private static let table = Table("location_series_data")
/// `location_series_data[series_identifier]` <-> `workout_activities[ROW_ID]`
private static let columnSeriesIdentifier = Expression<Int>("series_identifier")
private static let columnTimestamp = Expression<Double>("timestamp")
private static let columnLongitude = Expression<Double>("longitude")
private static let columnLatitude = Expression<Double>("latitude")
private static let columnAltitude = Expression<Double>("altitude")
private static let columnSpeed = Expression<Double>("speed")
private static let columnCourse = Expression<Double>("course")
private static let columnHorizontalAccuracy = Expression<Double>("horizontal_accuracy")
private static let columnVerticalAccuracy = Expression<Double>("vertical_accuracy")
private static let columnSpeedAccuracy = Expression<Double>("speed_accuracy")
private static let columnCourseAccuracy = Expression<Double>("course_accuracy")
private static let columnSignalEnvironment = Expression<Double>("signal_environment")
static func locationSamples(for seriesId: Int, in database: Database) throws -> [LocationSample] {
try database.prepare(table.filter(columnSeriesIdentifier == seriesId)).map(location)
}
static func locationSampleCount(for seriesId: Int, in database: Database) throws -> Int {
try database.scalar(table.filter(columnSeriesIdentifier == seriesId).count)
}
static func locationSamples(from start: Date, to end: Date, in database: Database) throws -> [LocationSample] {
let startTime = start.timeIntervalSinceReferenceDate
let endTime = end.timeIntervalSinceReferenceDate
return try database.prepare(table.filter(columnTimestamp >= startTime && columnTimestamp <= endTime)).map(location)
}
static func locationSampleCount(from start: Date, to end: Date, in database: Database) throws -> Int {
let startTime = start.timeIntervalSinceReferenceDate
let endTime = end.timeIntervalSinceReferenceDate
return try database.scalar(table.filter(columnTimestamp >= startTime && columnTimestamp <= endTime).count)
}
private static func location(row: Row) -> LocationSample {
.init(
coordinate: .init(
latitude: row[columnLatitude],
longitude: row[columnLongitude]),
altitude: row[columnAltitude],
horizontalAccuracy: row[columnHorizontalAccuracy],
verticalAccuracy: row[columnHorizontalAccuracy],
course: row[columnCourse],
courseAccuracy: row[columnCourseAccuracy],
speed: row[columnSpeed],
speedAccuracy: row[columnSpeedAccuracy],
timestamp: .init(timeIntervalSinceReferenceDate: row[columnTimestamp]),
sourceInfo: .init())
}
static func createTable(in database: Connection) throws {
try database.execute("CREATE TABLE location_series_data (series_identifier INTEGER NOT NULL REFERENCES data_series(hfd_key) DEFERRABLE INITIALLY DEFERRED, timestamp REAL NOT NULL, longitude REAL NOT NULL, latitude REAL NOT NULL, altitude REAL NOT NULL, speed REAL NOT NULL, course REAL NOT NULL, horizontal_accuracy REAL NOT NULL, vertical_accuracy REAL NOT NULL, speed_accuracy REAL NOT NULL, course_accuracy REAL NOT NULL, signal_environment INTEGER NOT NULL, PRIMARY KEY (series_identifier, timestamp)) WITHOUT ROWID")
}
static func insert(_ sample: LocationSample, in database: Connection, seriesId: Int) throws {
try database.run(table.insert(
columnSeriesIdentifier <- seriesId,
columnTimestamp <- sample.timestamp.timeIntervalSinceReferenceDate,
columnLongitude <- sample.coordinate.longitude,
columnLatitude <- sample.coordinate.latitude,
columnAltitude <- sample.altitude,
columnSpeed <- sample.speed,
columnCourse <- sample.course,
columnHorizontalAccuracy <- sample.horizontalAccuracy,
columnHorizontalAccuracy <- sample.verticalAccuracy,
columnSpeedAccuracy <- sample.speedAccuracy,
columnCourseAccuracy <- sample.courseAccuracy,
columnSignalEnvironment <- 1
))
}
}

View File

@ -0,0 +1,13 @@
import Foundation
import SQLite
struct DataSeriesTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("data_series")
}

View File

@ -0,0 +1,98 @@
import Foundation
import SQLite
import CoreLocation
typealias LocationSample = CLLocation
struct LocationSeriesDataTable {
private let database: Connection
init(database: Connection) {
self.database = database
}
let table = Table("location_series_data")
/// `location_series_data[series_identifier]` <-> `workout_activities[ROW_ID]`
let seriesIdentifier = Expression<Int>("series_identifier")
let timestamp = Expression<Double>("timestamp")
let longitude = Expression<Double>("longitude")
let latitude = Expression<Double>("latitude")
let altitude = Expression<Double>("altitude")
let speed = Expression<Double>("speed")
let course = Expression<Double>("course")
let horizontalAccuracy = Expression<Double>("horizontal_accuracy")
let verticalAccuracy = Expression<Double>("vertical_accuracy")
let speedAccuracy = Expression<Double>("speed_accuracy")
let courseAccuracy = Expression<Double>("course_accuracy")
let signalEnvironment = Expression<Double>("signal_environment")
func locationSamples(for seriesId: Int) throws -> [LocationSample] {
try database.prepare(table.filter(seriesIdentifier == seriesId)).map(location)
}
func locationSampleCount(for seriesId: Int) throws -> Int {
try database.scalar(table.filter(seriesIdentifier == seriesId).count)
}
func locationSamples(from start: Date, to end: Date) throws -> [LocationSample] {
let startTime = start.timeIntervalSinceReferenceDate
let endTime = end.timeIntervalSinceReferenceDate
return try database.prepare(table.filter(timestamp >= startTime && timestamp <= endTime)).map(location)
}
func locationSampleCount(from start: Date, to end: Date) throws -> Int {
let startTime = start.timeIntervalSinceReferenceDate
let endTime = end.timeIntervalSinceReferenceDate
return try database.scalar(table.filter(timestamp >= startTime && timestamp <= endTime).count)
}
func location(row: Row) -> LocationSample {
.init(
coordinate: .init(
latitude: row[latitude],
longitude: row[longitude]),
altitude: row[altitude],
horizontalAccuracy: row[horizontalAccuracy],
verticalAccuracy: row[horizontalAccuracy],
course: row[course],
courseAccuracy: row[courseAccuracy],
speed: row[speed],
speedAccuracy: row[speedAccuracy],
timestamp: .init(timeIntervalSinceReferenceDate: row[timestamp]),
sourceInfo: .init())
}
func create(references dataSeries: DataSeriesTable) throws {
try database.execute("CREATE TABLE location_series_data (series_identifier INTEGER NOT NULL REFERENCES data_series(hfd_key) DEFERRABLE INITIALLY DEFERRED, timestamp REAL NOT NULL, longitude REAL NOT NULL, latitude REAL NOT NULL, altitude REAL NOT NULL, speed REAL NOT NULL, course REAL NOT NULL, horizontal_accuracy REAL NOT NULL, vertical_accuracy REAL NOT NULL, speed_accuracy REAL NOT NULL, course_accuracy REAL NOT NULL, signal_environment INTEGER NOT NULL, PRIMARY KEY (series_identifier, timestamp)) WITHOUT ROWID")
}
func insert(_ sample: LocationSample, seriesId: Int) throws {
try database.run(table.insert(
seriesIdentifier <- seriesId,
timestamp <- sample.timestamp.timeIntervalSinceReferenceDate,
longitude <- sample.coordinate.longitude,
latitude <- sample.coordinate.latitude,
altitude <- sample.altitude,
speed <- sample.speed,
course <- sample.course,
horizontalAccuracy <- sample.horizontalAccuracy,
horizontalAccuracy <- sample.verticalAccuracy,
speedAccuracy <- sample.speedAccuracy,
courseAccuracy <- sample.courseAccuracy,
signalEnvironment <- 1
))
}
}