-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCustomQuery+Retention.swift
More file actions
198 lines (171 loc) · 8.85 KB
/
CustomQuery+Retention.swift
File metadata and controls
198 lines (171 loc) · 8.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import Foundation
import DateOperations
extension CustomQuery {
func precompiledRetentionQuery() throws -> CustomQuery {
var query = self
// Get the query intervals - we need at least one interval
guard let queryIntervals = intervals ?? relativeIntervals?.map({ QueryTimeInterval.from(relativeTimeInterval: $0) }),
let firstInterval = queryIntervals.first else {
throw QueryGenerationError.keyMissing(reason: "Missing intervals for retention query")
}
let beginDate = firstInterval.beginningDate
let endDate = firstInterval.endDate
// Use the query's granularity to determine retention period, defaulting to month if not specified
let retentionGranularity = query.granularity ?? .month
// Validate minimum interval based on granularity
try validateMinimumInterval(from: beginDate, to: endDate, granularity: retentionGranularity)
// Split into intervals based on the specified granularity
let retentionIntervals = try splitIntoIntervals(from: beginDate, to: endDate, granularity: retentionGranularity)
// Generate Aggregators
var aggregators = [Aggregator]()
for interval in retentionIntervals {
aggregators.append(aggregator(for: interval))
}
// Generate Post-Aggregators
var postAggregators = [PostAggregator]()
for row in retentionIntervals {
for column in retentionIntervals where column >= row {
postAggregators.append(postAggregatorBetween(interval1: row, interval2: column))
}
}
// Set the query properties
query.queryType = .groupBy
query.granularity = .all
query.aggregations = uniqued(aggregators)
query.postAggregations = uniqued(postAggregators)
return query
}
private func uniqued<T: Hashable>(_ array: [T]) -> [T] {
var set = Set<T>()
return array.filter { set.insert($0).inserted }
}
// MARK: - Helper Methods
private func validateMinimumInterval(from beginDate: Date, to endDate: Date, granularity: QueryGranularity) throws {
let calendar = Calendar.current
switch granularity {
case .day:
let components = calendar.dateComponents([.day], from: beginDate, to: endDate)
if (components.day ?? 0) < 1 {
throw QueryGenerationError.notImplemented(reason: "Daily retention queries require at least one day between begin and end dates")
}
case .week:
let components = calendar.dateComponents([.weekOfYear], from: beginDate, to: endDate)
if (components.weekOfYear ?? 0) < 1 {
throw QueryGenerationError.notImplemented(reason: "Weekly retention queries require at least one week between begin and end dates")
}
case .month:
let components = calendar.dateComponents([.month], from: beginDate, to: endDate)
if (components.month ?? 0) < 1 {
throw QueryGenerationError.notImplemented(reason: "Monthly retention queries require at least one month between begin and end dates")
}
case .quarter:
let components = calendar.dateComponents([.quarter], from: beginDate, to: endDate)
if (components.quarter ?? 0) < 1 {
throw QueryGenerationError.notImplemented(reason: "Quarterly retention queries require at least one quarter between begin and end dates")
}
case .year:
let components = calendar.dateComponents([.year], from: beginDate, to: endDate)
if (components.year ?? 0) < 1 {
throw QueryGenerationError.notImplemented(reason: "Yearly retention queries require at least one year between begin and end dates")
}
default:
throw QueryGenerationError.notImplemented(reason: "Retention queries support day, week, month, quarter, or year granularity")
}
}
private func splitIntoIntervals(from fromDate: Date, to toDate: Date, granularity: QueryGranularity) throws -> [DateInterval] {
let calendar = Calendar.current
var intervals = [DateInterval]()
switch granularity {
case .day:
let numberOfDays = numberOfUnitsBetween(beginDate: fromDate, endDate: toDate, component: .day)
for day in 0...numberOfDays {
guard let date = calendar.date(byAdding: .day, value: day, to: fromDate) else { continue }
let startOfDay = date.beginning(of: .day) ?? date
let endOfDay = startOfDay.end(of: .day) ?? startOfDay
intervals.append(DateInterval(start: startOfDay, end: endOfDay))
}
case .week:
let numberOfWeeks = numberOfUnitsBetween(beginDate: fromDate, endDate: toDate, component: .weekOfYear)
for week in 0...numberOfWeeks {
guard let date = calendar.date(byAdding: .weekOfYear, value: week, to: fromDate) else { continue }
let startOfWeek = date.beginning(of: .weekOfYear) ?? date
let endOfWeek = startOfWeek.end(of: .weekOfYear) ?? startOfWeek
intervals.append(DateInterval(start: startOfWeek, end: endOfWeek))
}
case .month:
let numberOfMonths = numberOfUnitsBetween(beginDate: fromDate, endDate: toDate, component: .month)
for month in 0...numberOfMonths {
guard let date = calendar.date(byAdding: .month, value: month, to: fromDate) else { continue }
let startOfMonth = date.beginning(of: .month) ?? date
let endOfMonth = startOfMonth.end(of: .month) ?? startOfMonth
intervals.append(DateInterval(start: startOfMonth, end: endOfMonth))
}
case .quarter:
let numberOfQuarters = numberOfUnitsBetween(beginDate: fromDate, endDate: toDate, component: .quarter)
for quarter in 0...numberOfQuarters {
guard let date = calendar.date(byAdding: .quarter, value: quarter, to: fromDate) else { continue }
let startOfQuarter = date.beginning(of: .quarter) ?? date
let endOfQuarter = startOfQuarter.end(of: .quarter) ?? startOfQuarter
intervals.append(DateInterval(start: startOfQuarter, end: endOfQuarter))
}
case .year:
let numberOfYears = numberOfUnitsBetween(beginDate: fromDate, endDate: toDate, component: .year)
for year in 0...numberOfYears {
guard let date = calendar.date(byAdding: .year, value: year, to: fromDate) else { continue }
let startOfYear = date.beginning(of: .year) ?? date
let endOfYear = startOfYear.end(of: .year) ?? startOfYear
intervals.append(DateInterval(start: startOfYear, end: endOfYear))
}
default:
throw QueryGenerationError.notImplemented(reason: "Retention queries support day, week, month, quarter, or year granularity")
}
return intervals
}
private func numberOfUnitsBetween(beginDate: Date, endDate: Date, component: Calendar.Component) -> Int {
let calendar = Calendar.current
let components = calendar.dateComponents([component], from: beginDate, to: endDate)
switch component {
case .day:
return components.day ?? 0
case .weekOfYear:
return components.weekOfYear ?? 0
case .month:
return components.month ?? 0
case .quarter:
return components.quarter ?? 0
case .year:
return components.year ?? 0
default:
return 0
}
}
private func title(for interval: DateInterval) -> String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate]
return "\(formatter.string(from: interval.start))_\(formatter.string(from: interval.end))"
}
private func aggregator(for interval: DateInterval) -> Aggregator {
.filtered(.init(
filter: .interval(.init(
dimension: "__time",
intervals: [.init(dateInterval: interval)]
)),
aggregator: .thetaSketch(.init(
name: "_\(title(for: interval))",
fieldName: "clientUser"
))
))
}
private func postAggregatorBetween(interval1: DateInterval, interval2: DateInterval) -> PostAggregator {
.thetaSketchEstimate(.init(
name: "retention_\(title(for: interval1))_\(title(for: interval2))",
field: .thetaSketchSetOp(.init(
func: .intersect,
fields: [
.fieldAccess(.init(type: .fieldAccess, fieldName: "_\(title(for: interval1))")),
.fieldAccess(.init(type: .fieldAccess, fieldName: "_\(title(for: interval2))")),
]
))
))
}
}