Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions OpenASO.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@
D32A00000000000000000001 /* OpenASOSchemaV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A00000000000000000011 /* OpenASOSchemaV5.swift */; };
D32A00000000000000000002 /* OpenASOSchemaV5.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A00000000000000000011 /* OpenASOSchemaV5.swift */; };
D32A00000000000000000003 /* KeywordResearchProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A00000000000000000012 /* KeywordResearchProject.swift */; };
D60000000000000000000001 /* OpenASOSchemaV6.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000011 /* OpenASOSchemaV6.swift */; };
D60000000000000000000002 /* RankingAppRevision.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000012 /* RankingAppRevision.swift */; };
D60000000000000000000003 /* RankingCrawlRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000013 /* RankingCrawlRecord.swift */; };
D60000000000000000000004 /* RankingFact.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000014 /* RankingFact.swift */; };
D60000000000000000000005 /* TrackedRankingCrawlLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000015 /* TrackedRankingCrawlLink.swift */; };
D60000000000000000000006 /* RankingMigrationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000016 /* RankingMigrationState.swift */; };
D60000000000000000000021 /* OpenASOSchemaV6.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000011 /* OpenASOSchemaV6.swift */; };
D60000000000000000000022 /* RankingAppRevision.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000012 /* RankingAppRevision.swift */; };
D60000000000000000000023 /* RankingCrawlRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000013 /* RankingCrawlRecord.swift */; };
D60000000000000000000024 /* RankingFact.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000014 /* RankingFact.swift */; };
D60000000000000000000025 /* TrackedRankingCrawlLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000015 /* TrackedRankingCrawlLink.swift */; };
D60000000000000000000026 /* RankingMigrationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000016 /* RankingMigrationState.swift */; };
D60000000000000000000007 /* SearchRanking/RankingAppRevisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000017 /* SearchRanking/RankingAppRevisionStore.swift */; };
D60000000000000000000027 /* SearchRanking/RankingAppRevisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000017 /* SearchRanking/RankingAppRevisionStore.swift */; };
D60000000000000000000008 /* Persistence/RankingSchemaV6Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000018 /* Persistence/RankingSchemaV6Migrator.swift */; };
D60000000000000000000028 /* Persistence/RankingSchemaV6Migrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000018 /* Persistence/RankingSchemaV6Migrator.swift */; };
D60000000000000000000009 /* RankingSchemaV6MigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000000000000000000019 /* RankingSchemaV6MigrationTests.swift */; };
D32A00000000000000000004 /* KeywordResearchProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A00000000000000000012 /* KeywordResearchProject.swift */; };
D32A00000000000000000005 /* KeywordResearchKeyword.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A00000000000000000013 /* KeywordResearchKeyword.swift */; };
D32A00000000000000000006 /* KeywordResearchKeyword.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32A00000000000000000013 /* KeywordResearchKeyword.swift */; };
Expand Down Expand Up @@ -440,6 +457,15 @@
D32A00000000000000000012 /* KeywordResearchProject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchProject.swift; sourceTree = "<group>"; };
D32A00000000000000000013 /* KeywordResearchKeyword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchKeyword.swift; sourceTree = "<group>"; };
D32A00000000000000000014 /* KeywordResearchPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchPersistenceTests.swift; sourceTree = "<group>"; };
D60000000000000000000011 /* OpenASOSchemaV6.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenASOSchemaV6.swift; sourceTree = "<group>"; };
D60000000000000000000012 /* RankingAppRevision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingAppRevision.swift; sourceTree = "<group>"; };
D60000000000000000000013 /* RankingCrawlRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingCrawlRecord.swift; sourceTree = "<group>"; };
D60000000000000000000014 /* RankingFact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingFact.swift; sourceTree = "<group>"; };
D60000000000000000000015 /* TrackedRankingCrawlLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackedRankingCrawlLink.swift; sourceTree = "<group>"; };
D60000000000000000000016 /* RankingMigrationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingMigrationState.swift; sourceTree = "<group>"; };
D60000000000000000000017 /* SearchRanking/RankingAppRevisionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRanking/RankingAppRevisionStore.swift; sourceTree = "<group>"; };
D60000000000000000000018 /* Persistence/RankingSchemaV6Migrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence/RankingSchemaV6Migrator.swift; sourceTree = "<group>"; };
D60000000000000000000019 /* RankingSchemaV6MigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingSchemaV6MigrationTests.swift; sourceTree = "<group>"; };
D32C00000000000000000011 /* Persistence/KeywordResearchProjectStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence/KeywordResearchProjectStore.swift; sourceTree = "<group>"; };
D32C00000000000000000012 /* KeywordResearchProjectStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordResearchProjectStoreTests.swift; sourceTree = "<group>"; };
D33A00000000000000000011 /* SearchRanking/KeywordResearchRankingWorkflow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRanking/KeywordResearchRankingWorkflow.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -623,6 +649,7 @@
D02000000000000000000003 /* PersistenceMigrationTests.swift */,
D02000000000000000000007 /* ReleasedV1StoreFixture.swift */,
D77BC4C40000000000000011 /* ExactV4StoreFixtureTests.swift */,
D60000000000000000000019 /* RankingSchemaV6MigrationTests.swift */,
D32A00000000000000000014 /* KeywordResearchPersistenceTests.swift */,
D32C00000000000000000012 /* KeywordResearchProjectStoreTests.swift */,
D33A00000000000000000012 /* KeywordResearchRankingWorkflowTests.swift */,
Expand Down Expand Up @@ -790,6 +817,8 @@
9838D42ED1A3EA1E6DDDD68C /* OpenASOError.swift */,
035877A6E7443B32A3980E39 /* SearchRanking/RankingMatcher.swift */,
26D4B2C87FDED9CFCFA50D7D /* SearchRanking/RankingRefreshCoordinator.swift */,
D60000000000000000000017 /* SearchRanking/RankingAppRevisionStore.swift */,
D60000000000000000000018 /* Persistence/RankingSchemaV6Migrator.swift */,
D33A00000000000000000011 /* SearchRanking/KeywordResearchRankingWorkflow.swift */,
D34A00000000000000000011 /* SearchRanking/KeywordResearchHistoryReader.swift */,
A70000000000000000000002 /* RatingsReviews/ReviewLanguageDetectionService.swift */,
Expand Down Expand Up @@ -980,6 +1009,12 @@
D09D00000000000000000001 /* OpenASOSchemaV3.swift */,
D20B00000000000000000011 /* OpenASOSchemaV4.swift */,
D32A00000000000000000011 /* OpenASOSchemaV5.swift */,
D60000000000000000000011 /* OpenASOSchemaV6.swift */,
D60000000000000000000012 /* RankingAppRevision.swift */,
D60000000000000000000013 /* RankingCrawlRecord.swift */,
D60000000000000000000014 /* RankingFact.swift */,
D60000000000000000000015 /* TrackedRankingCrawlLink.swift */,
D60000000000000000000016 /* RankingMigrationState.swift */,
D32A00000000000000000012 /* KeywordResearchProject.swift */,
D32A00000000000000000013 /* KeywordResearchKeyword.swift */,
D20B00000000000000000012 /* EstimatedKeywordDifficultyMetric.swift */,
Expand Down Expand Up @@ -1256,6 +1291,14 @@
D09D00000000000000000003 /* OpenASOSchemaV3.swift in Sources */,
D20B00000000000000000001 /* OpenASOSchemaV4.swift in Sources */,
D32A00000000000000000001 /* OpenASOSchemaV5.swift in Sources */,
D60000000000000000000001 /* OpenASOSchemaV6.swift in Sources */,
D60000000000000000000002 /* RankingAppRevision.swift in Sources */,
D60000000000000000000003 /* RankingCrawlRecord.swift in Sources */,
D60000000000000000000004 /* RankingFact.swift in Sources */,
D60000000000000000000005 /* TrackedRankingCrawlLink.swift in Sources */,
D60000000000000000000006 /* RankingMigrationState.swift in Sources */,
D60000000000000000000007 /* SearchRanking/RankingAppRevisionStore.swift in Sources */,
D60000000000000000000008 /* Persistence/RankingSchemaV6Migrator.swift in Sources */,
D32A00000000000000000003 /* KeywordResearchProject.swift in Sources */,
D32A00000000000000000005 /* KeywordResearchKeyword.swift in Sources */,
D20B00000000000000000003 /* EstimatedKeywordDifficultyMetric.swift in Sources */,
Expand Down Expand Up @@ -1388,6 +1431,7 @@
D13000000000000000000002 /* AppMetadataRefreshServiceTests.swift in Sources */,
D35A00000000000000000003 /* HeadlessRefreshServiceTests.swift in Sources */,
D35D00000000000000000003 /* HeadlessRefreshObservationTests.swift in Sources */,
D60000000000000000000009 /* RankingSchemaV6MigrationTests.swift in Sources */,
0A5C00000000000000000005 /* AppStoreConnectReviewServiceTests.swift in Sources */,
0A5C00000000000000000007 /* AppStoreWebMetadataProviderTests.swift in Sources */,
A19000000000000000000003 /* AppStoreWebRankingProviderTests.swift in Sources */,
Expand Down Expand Up @@ -1436,6 +1480,14 @@
D09D00000000000000000005 /* OpenASOSchemaV3.swift in Sources */,
D20B00000000000000000002 /* OpenASOSchemaV4.swift in Sources */,
D32A00000000000000000002 /* OpenASOSchemaV5.swift in Sources */,
D60000000000000000000021 /* OpenASOSchemaV6.swift in Sources */,
D60000000000000000000022 /* RankingAppRevision.swift in Sources */,
D60000000000000000000023 /* RankingCrawlRecord.swift in Sources */,
D60000000000000000000024 /* RankingFact.swift in Sources */,
D60000000000000000000025 /* TrackedRankingCrawlLink.swift in Sources */,
D60000000000000000000026 /* RankingMigrationState.swift in Sources */,
D60000000000000000000027 /* SearchRanking/RankingAppRevisionStore.swift in Sources */,
D60000000000000000000028 /* Persistence/RankingSchemaV6Migrator.swift in Sources */,
D32A00000000000000000004 /* KeywordResearchProject.swift in Sources */,
D32A00000000000000000006 /* KeywordResearchKeyword.swift in Sources */,
D20B00000000000000000004 /* EstimatedKeywordDifficultyMetric.swift in Sources */,
Expand Down
17 changes: 11 additions & 6 deletions OpenASO/Features/AppDetail/AppDetailPreviewFixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private struct AppDetailPreviewHarness: View {
platform: .iphone,
in: modelContext
)
let observation = KeywordRankingCrawl(
let observation = RankingCrawlRecord(
keyword: keyword,
storefront: storefront,
platform: .iphone,
Expand All @@ -238,16 +238,21 @@ private struct AppDetailPreviewHarness: View {

let rank = max(1, min(9, keyword.count % 10))
for (index, app) in rankedApps(rank: rank, competitors: competitors).enumerated() {
let item = KeywordAppRanking(
position: index + 1,
appStoreID: app.id == 6448311069 ? trackedAppID : app.id,
let rankingAppStoreID = app.id == 6448311069 ? trackedAppID : app.id
let revision = RankingAppRevision(
appStoreID: rankingAppStoreID,
bundleID: app.bundleID,
name: app.name,
subtitle: nil,
sellerName: app.seller,
sellerName: app.seller
)
let item = RankingFact(
position: index + 1,
appStoreID: rankingAppStoreID,
revision: revision,
observation: observation
)
observation.items.append(item)
modelContext.insert(revision)
modelContext.insert(item)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,26 @@ enum KeywordRankingHistoryLoader {
) throws -> [KeywordRankingCrawlSummary] {
let targetQueryKey = queryKey
let targetAppStoreID = appStoreID
let crawlDescriptor = FetchDescriptor<KeywordRankingCrawl>(
let crawlDescriptor = FetchDescriptor<RankingCrawlRecord>(
predicate: #Predicate { crawl in
crawl.queryKey == targetQueryKey
},
sortBy: [
SortDescriptor(\KeywordRankingCrawl.observedAt, order: .forward),
SortDescriptor(\KeywordRankingCrawl.observationKey, order: .forward)
SortDescriptor(\RankingCrawlRecord.observedAt, order: .forward),
SortDescriptor(\RankingCrawlRecord.observationKey, order: .forward)
]
)
let crawls = try modelContext.fetch(crawlDescriptor)
.filter { !$0.isTrackedRecovery }

guard !crawls.isEmpty else {
return []
}

let rankingDescriptor = FetchDescriptor<KeywordAppRanking>(
let rankingDescriptor = FetchDescriptor<RankingFact>(
predicate: #Predicate { ranking in
ranking.queryKey == targetQueryKey && ranking.appStoreID == targetAppStoreID
ranking.observation.queryKey == targetQueryKey
&& ranking.appStoreID == targetAppStoreID
}
)
let rankings = try modelContext.fetch(rankingDescriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ enum KeywordRankingListLoader {
}

let targetCrawlKey = crawlKey
let descriptor = FetchDescriptor<KeywordAppRanking>(
let descriptor = FetchDescriptor<RankingFact>(
predicate: #Predicate { ranking in
ranking.crawlKey == targetCrawlKey
ranking.observation.observationKey == targetCrawlKey
},
sortBy: [SortDescriptor(\KeywordAppRanking.position, order: .forward)]
sortBy: [SortDescriptor(\RankingFact.position, order: .forward)]
)

let rankings = try modelContext.fetch(descriptor)
Expand Down Expand Up @@ -238,25 +238,31 @@ enum KeywordRankingListLoader {
let uniqueAppStoreIDs = Array(Set(appStoreIDs)).sorted()
snapshotsByID.reserveCapacity(uniqueAppStoreIDs.count)

for appStoreID in uniqueAppStoreIDs {
try checkCancellation()
let targetAppStoreID = appStoreID
var descriptor = FetchDescriptor<AppDailyRating>(
predicate: #Predicate { snapshot in
snapshot.storefront == targetStorefront
&& snapshot.appStoreID == targetAppStoreID
&& snapshot.ratingCount != nil
},
sortBy: [
SortDescriptor(\.ratingDate, order: .reverse),
SortDescriptor(\.observedAt, order: .reverse)
]
)
descriptor.fetchLimit = ratingSnapshotLimit
let snapshots = try modelContext.fetch(descriptor)
try checkCancellation()
guard !snapshots.isEmpty else { continue }
snapshotsByID[appStoreID] = snapshots.reversed().map(RatingSnapshotDisplayValue.init)
guard !uniqueAppStoreIDs.isEmpty else { return [:] }
let descriptor = FetchDescriptor<AppDailyRating>(
predicate: #Predicate { snapshot in
snapshot.storefront == targetStorefront
&& uniqueAppStoreIDs.contains(snapshot.appStoreID)
&& snapshot.ratingCount != nil
},
sortBy: [
SortDescriptor(\.appStoreID, order: .forward),
SortDescriptor(\.ratingDate, order: .reverse),
SortDescriptor(\.observedAt, order: .reverse)
]
)
for (index, snapshot) in try modelContext.fetch(descriptor).enumerated() {
if snapshotsByID[snapshot.appStoreID, default: []].count < ratingSnapshotLimit {
snapshotsByID[snapshot.appStoreID, default: []].append(
RatingSnapshotDisplayValue(snapshot)
)
}
if index.isMultiple(of: 256) {
try checkCancellation()
}
}
for appStoreID in snapshotsByID.keys {
snapshotsByID[appStoreID]?.reverse()
}

return snapshotsByID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ struct KeywordRankingCrawlSummary: Identifiable, Equatable, Sendable {
)
}

init(crawl: KeywordRankingCrawl, rank: Int?, errorMessage: String? = nil) {
init(crawl: RankingCrawlRecord, rank: Int?, errorMessage: String? = nil) {
self.init(
id: crawl.observationKey,
rank: rank,
Expand Down Expand Up @@ -724,7 +724,7 @@ struct KeywordRankingAppSummary: Identifiable, Equatable, Sendable {
)
}

init(_ ranking: KeywordAppRanking) {
init(_ ranking: RankingFact) {
self.init(
position: ranking.position,
appStoreID: ranking.appStoreID,
Expand Down
1 change: 1 addition & 0 deletions OpenASO/Models/KeywordAppRanking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ final class KeywordAppRanking {
}

typealias KeywordAppRanking = OpenASOSchemaV1.KeywordAppRanking
typealias LegacyKeywordAppRanking = OpenASOSchemaV1.KeywordAppRanking
1 change: 1 addition & 0 deletions OpenASO/Models/KeywordRankingCrawl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,4 @@ final class KeywordRankingCrawl {
}

typealias KeywordRankingCrawl = OpenASOSchemaV1.KeywordRankingCrawl
typealias LegacyKeywordRankingCrawl = OpenASOSchemaV1.KeywordRankingCrawl
7 changes: 6 additions & 1 deletion OpenASO/Models/OpenASOSchemaV1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ enum OpenASOMigrationPlan: SchemaMigrationPlan {
OpenASOSchemaV2.self,
OpenASOSchemaV3.self,
OpenASOSchemaV4.self,
OpenASOSchemaV5.self
OpenASOSchemaV5.self,
OpenASOSchemaV6.self
]
}

Expand All @@ -68,6 +69,10 @@ enum OpenASOMigrationPlan: SchemaMigrationPlan {
.lightweight(
fromVersion: OpenASOSchemaV4.self,
toVersion: OpenASOSchemaV5.self
),
.lightweight(
fromVersion: OpenASOSchemaV5.self,
toVersion: OpenASOSchemaV6.self
)
]
}
Expand Down
20 changes: 20 additions & 0 deletions OpenASO/Models/OpenASOSchemaV6.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwiftData

/// Adds a normalized ranking store alongside the frozen V1 ranking entities.
/// A resumable post-open migration copies and validates legacy ranking data
/// before the redundant V1 rows are removed.
enum OpenASOSchemaV6: VersionedSchema {
static var versionIdentifier: Schema.Version {
Schema.Version(6, 0, 0)
}

static var models: [any PersistentModel.Type] {
OpenASOSchemaV5.models + [
RankingAppRevision.self,
RankingCrawlRecord.self,
RankingFact.self,
TrackedRankingCrawlLink.self,
RankingMigrationState.self
]
}
}
Loading
Loading