From 656cbaaf10f97dae6c0c9562df6641b942a1c28b Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Fri, 2 Feb 2024 14:53:45 +0100 Subject: [PATCH] Refactor metadata tables --- HealthImport.xcodeproj/project.pbxproj | 45 ++----- .../xcshareddata/swiftpm/Package.resolved | 9 -- HealthImport/Model/Metadata.swift | 29 ---- HealthImport/Model/MetadataKey+SQLite.swift | 38 ------ HealthImport/Model/MetadataKey.swift | 6 +- HealthImport/Model/MetadataValue+SQLite.swift | 124 ----------------- .../Model/Tables/MetadataKeysTable.swift | 44 ++++++ .../Model/Tables/MetadataTables.swift | 42 ++++++ .../Model/Tables/MetadataValuesTable.swift | 126 ++++++++++++++++++ HealthImport/Model/Tables/WorkoutsTable.swift | 9 +- 10 files changed, 236 insertions(+), 236 deletions(-) delete mode 100644 HealthImport/Model/Metadata.swift delete mode 100644 HealthImport/Model/MetadataKey+SQLite.swift delete mode 100644 HealthImport/Model/MetadataValue+SQLite.swift create mode 100644 HealthImport/Model/Tables/MetadataKeysTable.swift create mode 100644 HealthImport/Model/Tables/MetadataTables.swift create mode 100644 HealthImport/Model/Tables/MetadataValuesTable.swift diff --git a/HealthImport.xcodeproj/project.pbxproj b/HealthImport.xcodeproj/project.pbxproj index 86ef8c3..e5192c5 100644 --- a/HealthImport.xcodeproj/project.pbxproj +++ b/HealthImport.xcodeproj/project.pbxproj @@ -28,7 +28,7 @@ 885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */; }; 8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */; }; 8850029D2B5D197300E7D4DB /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029C2B5D197300E7D4DB /* EventDetailView.swift */; }; - 8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */; }; + 8850029F2B5D1C7000E7D4DB /* MetadataValuesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */; }; 885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885002A22B5D217600E7D4DB /* MetadataValue.swift */; }; 885002A62B5D296700E7D4DB /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A52B5D296700E7D4DB /* Collections */; }; 885002A82B5D296700E7D4DB /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 885002A72B5D296700E7D4DB /* DequeModule */; }; @@ -36,8 +36,8 @@ E201EC732B626A30005B83D3 /* WorkoutActivity+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */; }; E201EC752B626B19005B83D3 /* Metadata+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC742B626B19005B83D3 /* Metadata+Mock.swift */; }; E201EC772B626FC1005B83D3 /* MetadataKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC762B626FC1005B83D3 /* MetadataKey.swift */; }; - E201EC792B627572005B83D3 /* MetadataKey+SQLite.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */; }; - E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* Metadata.swift */; }; + E201EC792B627572005B83D3 /* MetadataKeysTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC782B627572005B83D3 /* MetadataKeysTable.swift */; }; + E201EC7B2B6275CA005B83D3 /* MetadataTables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */; }; 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 */; }; @@ -56,7 +56,6 @@ E27BC6942B5FD587003A8873 /* Workout+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6932B5FD587003A8873 /* Workout+Mock.swift */; }; E27BC6962B5FD61D003A8873 /* WorkoutEvent+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6952B5FD61D003A8873 /* WorkoutEvent+Mock.swift */; }; E27BC6982B5FD76F003A8873 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27BC6972B5FD76F003A8873 /* Data+Extensions.swift */; }; - E2FDFF162B6AFD990080A7B3 /* BinaryCodable in Frameworks */ = {isa = PBXBuildFile; productRef = E2FDFF152B6AFD990080A7B3 /* BinaryCodable */; }; E2FDFF182B6BB61D0080A7B3 /* HKHealthStoreInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF172B6BB61D0080A7B3 /* HKHealthStoreInterface.swift */; }; E2FDFF1A2B6BB6A40080A7B3 /* HKHealthStore+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF192B6BB6A40080A7B3 /* HKHealthStore+Interface.swift */; }; E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FDFF1B2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift */; }; @@ -91,13 +90,13 @@ 885002982B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKWorkoutSwimmingLocationType+Extensions.swift"; sourceTree = ""; }; 8850029A2B5D16E200E7D4DB /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; 8850029C2B5D197300E7D4DB /* EventDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailView.swift; sourceTree = ""; }; - 8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetadataValue+SQLite.swift"; sourceTree = ""; }; + 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValuesTable.swift; sourceTree = ""; }; 885002A22B5D217600E7D4DB /* MetadataValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataValue.swift; sourceTree = ""; }; E201EC722B626A30005B83D3 /* WorkoutActivity+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WorkoutActivity+Mock.swift"; sourceTree = ""; }; E201EC742B626B19005B83D3 /* Metadata+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Metadata+Mock.swift"; sourceTree = ""; }; E201EC762B626FC1005B83D3 /* MetadataKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKey.swift; sourceTree = ""; }; - E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MetadataKey+SQLite.swift"; sourceTree = ""; }; - E201EC7A2B6275CA005B83D3 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; + E201EC782B627572005B83D3 /* MetadataKeysTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataKeysTable.swift; sourceTree = ""; }; + E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataTables.swift; sourceTree = ""; }; E201EC7C2B62930E005B83D3 /* SamplesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SamplesTable.swift; sourceTree = ""; }; E201EC7E2B629B4C005B83D3 /* SampleListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleListView.swift; sourceTree = ""; }; E201EC802B631708005B83D3 /* Goal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Goal.swift; sourceTree = ""; }; @@ -132,7 +131,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E2FDFF162B6AFD990080A7B3 /* BinaryCodable in Frameworks */, 885002A62B5D296700E7D4DB /* Collections in Frameworks */, 885002772B5C2FC400E7D4DB /* SQLite in Frameworks */, 885002AA2B5D296700E7D4DB /* OrderedCollections in Frameworks */, @@ -212,11 +210,8 @@ E2FDFF232B6C509D0080A7B3 /* Tables */, E2FDFF212B6BE35B0080A7B3 /* EventMetadata.pb.swift */, E201EC802B631708005B83D3 /* Goal.swift */, - E201EC7A2B6275CA005B83D3 /* Metadata.swift */, E201EC762B626FC1005B83D3 /* MetadataKey.swift */, - E201EC782B627572005B83D3 /* MetadataKey+SQLite.swift */, 885002A22B5D217600E7D4DB /* MetadataValue.swift */, - 8850029E2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift */, E27BC6852B5FBF0B003A8873 /* Sample.swift */, 8850027E2B5C36A700E7D4DB /* Workout.swift */, E2FDFF2A2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift */, @@ -255,8 +250,12 @@ E2FDFF232B6C509D0080A7B3 /* Tables */ = { isa = PBXGroup; children = ( - E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */, E2FDFF262B6C56C70080A7B3 /* DataProvenancesTable.swift */, + E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */, + E27BC6792B5D99AC003A8873 /* LocationSeriesDataTable.swift */, + E201EC782B627572005B83D3 /* MetadataKeysTable.swift */, + E201EC7A2B6275CA005B83D3 /* MetadataTables.swift */, + 8850029E2B5D1C7000E7D4DB /* MetadataValuesTable.swift */, E2FDFF242B6C50A80080A7B3 /* ObjectsTable.swift */, E27BC6872B5FC220003A8873 /* QuantitySamplesTable.swift */, E201EC7C2B62930E005B83D3 /* SamplesTable.swift */, @@ -264,7 +263,6 @@ E27BC68F2B5FCEA4003A8873 /* WorkoutActivitiesTable.swift */, E27BC68D2B5FCBD5003A8873 /* WorkoutEventsTable.swift */, 8850027A2B5C35BF00E7D4DB /* WorkoutsTable.swift */, - E2FDFF2C2B6D23670080A7B3 /* DataSeriesTable.swift */, ); path = Tables; sourceTree = ""; @@ -290,7 +288,6 @@ 885002A52B5D296700E7D4DB /* Collections */, 885002A72B5D296700E7D4DB /* DequeModule */, 885002A92B5D296700E7D4DB /* OrderedCollections */, - E2FDFF152B6AFD990080A7B3 /* BinaryCodable */, E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */, ); productName = HealthImport; @@ -324,7 +321,6 @@ packageReferences = ( 885002752B5C2FC400E7D4DB /* XCRemoteSwiftPackageReference "SQLite" */, 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */, - E2FDFF142B6AFD990080A7B3 /* XCRemoteSwiftPackageReference "BinaryCodable" */, E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */, ); productRefGroup = 885002582B5C273C00E7D4DB /* Products */; @@ -361,7 +357,7 @@ 8850029B2B5D16E200E7D4DB /* TimeInterval+Extensions.swift in Sources */, 885002972B5D157900E7D4DB /* HKWorkoutSessionLocationType+Extensions.swift in Sources */, 885002792B5C320400E7D4DB /* Optional+Extensions.swift in Sources */, - E201EC792B627572005B83D3 /* MetadataKey+SQLite.swift in Sources */, + E201EC792B627572005B83D3 /* MetadataKeysTable.swift in Sources */, E2FDFF252B6C50A80080A7B3 /* ObjectsTable.swift in Sources */, 885002992B5D15D200E7D4DB /* HKWorkoutSwimmingLocationType+Extensions.swift in Sources */, E27BC6822B5E762D003A8873 /* LocationSampleDetailView.swift in Sources */, @@ -378,7 +374,7 @@ 885002712B5C299900E7D4DB /* HealthDatabase.swift in Sources */, E27BC6842B5E76A4003A8873 /* Location+Mock.swift in Sources */, 885002932B5D129300E7D4DB /* ActivityDetailView.swift in Sources */, - 8850029F2B5D1C7000E7D4DB /* MetadataValue+SQLite.swift in Sources */, + 8850029F2B5D1C7000E7D4DB /* MetadataValuesTable.swift in Sources */, E2FDFF2B2B6D1E5E0080A7B3 /* HKWorkoutActivity+Comparable.swift in Sources */, E27BC6882B5FC220003A8873 /* QuantitySamplesTable.swift in Sources */, E201EC7D2B62930E005B83D3 /* SamplesTable.swift in Sources */, @@ -388,7 +384,7 @@ 8850028D2B5D0B5000E7D4DB /* WorkoutDetailView.swift in Sources */, E2FDFF1C2B6BD0D20080A7B3 /* HKDatabaseFile+Interface.swift in Sources */, 885002A32B5D217600E7D4DB /* MetadataValue.swift in Sources */, - E201EC7B2B6275CA005B83D3 /* Metadata.swift in Sources */, + E201EC7B2B6275CA005B83D3 /* MetadataTables.swift in Sources */, E27BC67A2B5D99AC003A8873 /* LocationSeriesDataTable.swift in Sources */, 885002952B5D147100E7D4DB /* DetailRow.swift in Sources */, E2FDFF292B6D10D60080A7B3 /* String+Extensions.swift in Sources */, @@ -628,14 +624,6 @@ minimumVersion = 1.0.6; }; }; - E2FDFF142B6AFD990080A7B3 /* XCRemoteSwiftPackageReference "BinaryCodable" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/christophhagen/BinaryCodable"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.3; - }; - }; E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -667,11 +655,6 @@ package = 885002A42B5D296700E7D4DB /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; - E2FDFF152B6AFD990080A7B3 /* BinaryCodable */ = { - isa = XCSwiftPackageProductDependency; - package = E2FDFF142B6AFD990080A7B3 /* XCRemoteSwiftPackageReference "BinaryCodable" */; - productName = BinaryCodable; - }; E2FDFF1F2B6BE34C0080A7B3 /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; package = E2FDFF1E2B6BE34C0080A7B3 /* XCRemoteSwiftPackageReference "swift-protobuf" */; diff --git a/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c0f89b5..61050a7 100644 --- a/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/HealthImport.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "binarycodable", - "kind" : "remoteSourceControl", - "location" : "https://github.com/christophhagen/BinaryCodable", - "state" : { - "revision" : "33643e2343f591368647935d14a94e35dc2d3bf0", - "version" : "2.0.3" - } - }, { "identity" : "sqlite.swift", "kind" : "remoteSourceControl", diff --git a/HealthImport/Model/Metadata.swift b/HealthImport/Model/Metadata.swift deleted file mode 100644 index 3be7b2e..0000000 --- a/HealthImport/Model/Metadata.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import SQLite - -enum Metadata { - - static func allKeys(in database: Database) throws -> [Int : Key] { - try Key.readAll(in: database) - } - - static func createTables(in database: Connection) throws { - try Value.createTable(in: database) - try Key.createTable(in: database) - } - - static func metadata(for workoutId: Int, in database: Connection, keyMap: [Int : Key]) throws -> [Key : Value] { - return try Value.metadata(for: workoutId, in: database).reduce(into: [:]) { dict, entry in - guard let key = keyMap[entry.keyId] else { - print("No '\(entry.keyId)' in table 'metadata_keys'") - return - } - dict[key] = entry.value - } - } - - static func insert(_ value: Value, for key: Key, of workoutId: Int, in database: Connection) throws { - let keyId = try Metadata.Key.hasKey(key, in: database) ?? Metadata.Key.insert(key: key, in: database) - try Value.insert(value, of: workoutId, for: keyId, in: database) - } -} diff --git a/HealthImport/Model/MetadataKey+SQLite.swift b/HealthImport/Model/MetadataKey+SQLite.swift deleted file mode 100644 index 7805fdd..0000000 --- a/HealthImport/Model/MetadataKey+SQLite.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import SQLite - -extension Metadata.Key { - - private static let table = Table("metadata_keys") - - private static let columnId = Expression("ROWID") - - private static let columnKey = Expression("key") - - static func key(for keyId: Int, in database: Connection) throws -> String { - try database.prepare(table.filter(columnId == keyId).limit(1)).map { $0[columnKey] }.first! - } - - static func readAll(in database: Connection) throws -> [Int : Self] { - try database.prepare(table).reduce(into: [:]) { dict, row in - dict[row[columnId]] = .init(rawValue: row[columnKey]) - } - } - - static func createTable(in database: Connection) throws { - //try database.execute("CREATE TABLE metadata_keys (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE)") - try database.run(table.create { table in - table.column(columnId, primaryKey: .autoincrement) - table.column(columnKey, unique: true) - }) - } - - static func hasKey(_ key: Self, in database: Connection) throws -> Int? { - try database.prepare(table.filter(columnKey == key.rawValue).limit(1)).map { $0[columnId] }.first - } - - static func insert(key: Self, in database: Connection) throws -> Int { - Int(try database.run(table.insert(columnKey <- key.rawValue))) - } - -} diff --git a/HealthImport/Model/MetadataKey.swift b/HealthImport/Model/MetadataKey.swift index 29b439d..1f03e8f 100644 --- a/HealthImport/Model/MetadataKey.swift +++ b/HealthImport/Model/MetadataKey.swift @@ -1,5 +1,9 @@ import Foundation +enum Metadata { + +} + extension Metadata { enum Key { @@ -80,7 +84,7 @@ extension Metadata { extension Metadata.Key: RawRepresentable { - init?(rawValue: String) { + init(rawValue: String) { switch rawValue { case "HKWasUserEntered": self = .wasUserEntered case "HKHeartRateSensorLocation": self = .heartRateSensorLocation diff --git a/HealthImport/Model/MetadataValue+SQLite.swift b/HealthImport/Model/MetadataValue+SQLite.swift deleted file mode 100644 index e07a9f3..0000000 --- a/HealthImport/Model/MetadataValue+SQLite.swift +++ /dev/null @@ -1,124 +0,0 @@ -import Foundation -import SQLite - -extension Metadata.Value { - - private static let table = Table("metadata_values") - - private static let columnRowId = Expression("ROW_ID") - - private static let columnKeyId = Expression("key_id") - - private static let columnObjectId = Expression("object_id") - - private static let columnValueType = Expression("value_type") - - private static let columnStringValue = Expression("string_value") - - private static let columnNumericalValue = Expression("numerical_value") - - private static let columnDateValue = Expression("date_value") - - private static let columnDataValue = Expression("data_value") - - private static func readAll(in database: Connection) throws -> [Self] { - try database.prepare(table).map(from) - } - - static func metadata(for workoutId: Int, in database: Connection) throws -> [Self] { - try database.prepare(table.filter(columnObjectId == workoutId)).map(from) - } - - static func metadata(for workoutId: Int, in database: Connection) throws -> [(keyId: Int, value: Self)] { - try database.prepare(table.filter(columnObjectId == workoutId)).compactMap { row in - guard let keyId = row[columnKeyId] else { - print("Found 'key_id == NULL' for metadata value of workout \(workoutId)") - return nil - } - return (keyId, from(row: row)) - } - } - - static func createTable(in database: Connection) throws { - //try database.execute("CREATE TABLE metadata_values (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key_id INTEGER, object_id INTEGER, value_type INTEGER NOT NULL DEFAULT 0, string_value TEXT, numerical_value REAL, date_value REAL, data_value BLOB)") - try database.run(table.create { table in - table.column(columnRowId, primaryKey: .autoincrement) - table.column(columnKeyId) - table.column(columnObjectId) - table.column(columnValueType, defaultValue: 0) - table.column(columnStringValue) - table.column(columnNumericalValue) - table.column(columnDateValue) - table.column(columnDataValue) - }) - } - - static func insert(_ element: Self, of workoutId: Int, for keyId: Int, in database: Connection) throws { - try database.run(table.insert( - columnKeyId <- keyId, - columnObjectId <- workoutId, - columnValueType <- element.valueType.rawValue, - columnStringValue <- element.stringValue, - columnNumericalValue <- element.numericalValue, - columnDateValue <- element.dateValue, - columnDataValue <- element.dataValue - )) - } -} - -private extension Metadata.Value { - - var stringValue: String? { - if case let .string(value) = self { - return value - } - if case let .numerical(_, unit) = self { - return unit - } - return nil - } - - var numericalValue: Double? { - if case let .number(value) = self { - return value - } - if case let .numerical(value: value, unit: _) = self { - return value - } - return nil - } - - var dateValue: Double? { - if case let .date(value: date) = self { - return date.timeIntervalSinceReferenceDate - } - return nil - } - - var dataValue: Data? { - if case let .data(data) = self { - return data - } - return nil - } -} - - -extension Metadata.Value { - - static func from(row: Row) -> Self { - let valueType = ValueType(rawValue: row[columnValueType])! - switch valueType { - case .string: - return .string(value: row[columnStringValue]!) - case .number: - return .number(value: row[columnNumericalValue]!) - case .date: - return .date(value: .init(timeIntervalSinceReferenceDate: row[columnDateValue]!)) - case .numerical: - return .numerical(value: row[columnNumericalValue]!, unit: row[columnStringValue]!) - case .data: - return .data(value: row[columnDataValue]!) - } - } -} diff --git a/HealthImport/Model/Tables/MetadataKeysTable.swift b/HealthImport/Model/Tables/MetadataKeysTable.swift new file mode 100644 index 0000000..e363cdb --- /dev/null +++ b/HealthImport/Model/Tables/MetadataKeysTable.swift @@ -0,0 +1,44 @@ +import Foundation +import SQLite + +struct MetadataKeysTable { + + private let database: Connection + + init(database: Connection) { + self.database = database + } + + let table = Table("metadata_keys") + + let rowId = Expression("ROWID") + + let key = Expression("key") + + func key(for keyId: Int, in database: Connection) throws -> String { + try database.pluck(table.filter(rowId == keyId)).map { $0[key] }! + } + + func all() throws -> [Int : Metadata.Key] { + try database.prepare(table).reduce(into: [:]) { dict, row in + dict[row[rowId]] = .init(rawValue: row[key]) + } + } + + func create() throws { + //try database.execute("CREATE TABLE metadata_keys (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE)") + try database.run(table.create { table in + table.column(rowId, primaryKey: .autoincrement) + table.column(key, unique: true) + }) + } + + func hasKey(_ key: Metadata.Key) throws -> Int? { + try database.pluck(table.filter(self.key == key.rawValue)).map { $0[rowId] } + } + + func insert(key: Metadata.Key) throws -> Int { + Int(try database.run(table.insert(self.key <- key.rawValue))) + } + +} diff --git a/HealthImport/Model/Tables/MetadataTables.swift b/HealthImport/Model/Tables/MetadataTables.swift new file mode 100644 index 0000000..e12b2d4 --- /dev/null +++ b/HealthImport/Model/Tables/MetadataTables.swift @@ -0,0 +1,42 @@ +import Foundation +import SQLite + +struct MetadataTables { + + private let database: Connection + + let values: MetadataValuesTable + + let keys: MetadataKeysTable + + init(database: Connection) { + self.database = database + self.keys = .init(database: database) + self.values = .init(database: database) + } + + func create() throws { + try values.create() + try keys.create() + } + + func metadata(for workoutId: Int) throws -> [Metadata.Key : Metadata.Value] { + // Keys: rowId -> String + + let selection = values.table + .select(values.table[*], keys.table[keys.key]) + .filter(values.objectId == workoutId) + .join(.leftOuter, keys.table, on: values.table[values.keyId] == keys.table[keys.rowId]) + + return try database.prepare(selection).reduce(into: [:]) { dict, row in + let key = Metadata.Key(rawValue: row[keys.key]) + let value = values.from(row: row) + dict[key] = value + } + } + + func insert(_ value: Metadata.Value, for key: Metadata.Key, of workoutId: Int) throws { + let keyId = try keys.hasKey(key) ?? keys.insert(key: key) + try values.insert(value, of: workoutId, for: keyId) + } +} diff --git a/HealthImport/Model/Tables/MetadataValuesTable.swift b/HealthImport/Model/Tables/MetadataValuesTable.swift new file mode 100644 index 0000000..4c8d8dc --- /dev/null +++ b/HealthImport/Model/Tables/MetadataValuesTable.swift @@ -0,0 +1,126 @@ +import Foundation +import SQLite + +struct MetadataValuesTable { + + private let database: Connection + + init(database: Connection) { + self.database = database + } + + let table = Table("metadata_values") + + let rowId = Expression("ROW_ID") + + let keyId = Expression("key_id") + + let objectId = Expression("object_id") + + let valueType = Expression("value_type") + + let stringValue = Expression("string_value") + + let numericalValue = Expression("numerical_value") + + let dateValue = Expression("date_value") + + let dataValue = Expression("data_value") + + func all() throws -> [Metadata.Value] { + try database.prepare(table).map(from) + } + + func metadata(for workoutId: Int) throws -> [Metadata.Value] { + try database.prepare(table.filter(objectId == workoutId)).map(from) + } + + func metadata(for workoutId: Int) throws -> [(keyId: Int, value: Metadata.Value)] { + try database.prepare(table.filter(objectId == workoutId)).compactMap { row in + guard let keyId = row[keyId] else { + print("Found 'key_id == NULL' for metadata value of workout \(workoutId)") + return nil + } + return (keyId, from(row: row)) + } + } + + func from(row: Row) -> Metadata.Value { + let valueType = Metadata.Value.ValueType(rawValue: row[valueType])! + switch valueType { + case .string: + return .string(value: row[stringValue]!) + case .number: + return .number(value: row[numericalValue]!) + case .date: + return .date(value: .init(timeIntervalSinceReferenceDate: row[dateValue]!)) + case .numerical: + return .numerical(value: row[numericalValue]!, unit: row[stringValue]!) + case .data: + return .data(value: row[dataValue]!) + } + } + + func create() throws { + //try database.execute("CREATE TABLE metadata_values (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, key_id INTEGER, object_id INTEGER, value_type INTEGER NOT NULL DEFAULT 0, string_value TEXT, numerical_value REAL, date_value REAL, data_value BLOB)") + try database.run(table.create { table in + table.column(rowId, primaryKey: .autoincrement) + table.column(keyId) + table.column(objectId) + table.column(valueType, defaultValue: 0) + table.column(stringValue) + table.column(numericalValue) + table.column(dateValue) + table.column(dataValue) + }) + } + + func insert(_ element: Metadata.Value, of workoutId: Int, for keyId: Int) throws { + try database.run(table.insert( + self.keyId <- keyId, + objectId <- workoutId, + valueType <- element.valueType.rawValue, + stringValue <- element.stringValue, + numericalValue <- element.numericalValue, + dateValue <- element.dateValue, + dataValue <- element.dataValue + )) + } +} + +private extension Metadata.Value { + + var stringValue: String? { + if case let .string(value) = self { + return value + } + if case let .numerical(_, unit) = self { + return unit + } + return nil + } + + var numericalValue: Double? { + if case let .number(value) = self { + return value + } + if case let .numerical(value: value, unit: _) = self { + return value + } + return nil + } + + var dateValue: Double? { + if case let .date(value: date) = self { + return date.timeIntervalSinceReferenceDate + } + return nil + } + + var dataValue: Data? { + if case let .data(data) = self { + return data + } + return nil + } +} diff --git a/HealthImport/Model/Tables/WorkoutsTable.swift b/HealthImport/Model/Tables/WorkoutsTable.swift index dedcbc5..7206c62 100644 --- a/HealthImport/Model/Tables/WorkoutsTable.swift +++ b/HealthImport/Model/Tables/WorkoutsTable.swift @@ -10,10 +10,13 @@ struct WorkoutsTable { let activities: WorkoutActivitiesTable + let metadata: MetadataTables + init(database: Connection) { self.database = database self.events = .init(database: database) self.activities = .init(database: database) + self.metadata = .init(database: database) } let table = Table("workouts") @@ -37,14 +40,12 @@ struct WorkoutsTable { let condenserDate = Expression("condenser_date") func workouts() throws -> [Workout] { - let metadataKeys = try Metadata.allKeys(in: database) - return try database.prepare(table).map { row in let id = row[dataId] let events = try events.events(for: id, in: database) let activities = try activities.activities(for: id) - let metadata = try Metadata.metadata(for: id, in: database, keyMap: metadataKeys) + let metadata = try metadata.metadata(for: id) return .init( id: id, totalDistance: row[totalDistance], @@ -94,7 +95,7 @@ struct WorkoutsTable { } for (key, value) in element.metadata { - try Metadata.insert(value, for: key, of: dataId, in: database) + try metadata.insert(value, for: key, of: dataId) } } }