Skip to content

Commit f1fe76e

Browse files
committed
feat: Add ListBaselineStatusCount to retrieve cumulative baseline feature counts
This PR introduces the `ListBaselineStatusCount` function and its associated helper functions to retrieve a cumulative count of baseline features over time. The `ListBaselineStatusCount` function works similarly to `ListBrowserFeatureCountMetric` by: 1. Calculating an initial cumulative count of baseline features up to the specified `startAt` date. 2. Retrieving subsequent baseline feature counts within the specified date range (`startAt` to `endAt`). 3. Accumulating these counts to provide a cumulative view of baseline feature adoption over time. Currently, the function only supports retrieving counts based on the `LowDate` field in the `FeatureBaselineStatus` table. However, support for `HighDate` can be easily added in the future by expanding the `BaselineDateType` enum and updating the query construction logic. This functionality is useful for tracking the overall adoption of baseline features and understanding how the baseline status of features changes over time.
1 parent 16ed164 commit f1fe76e

File tree

2 files changed

+407
-0
lines changed

2 files changed

+407
-0
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package gcpspanner
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"time"
8+
9+
"cloud.google.com/go/spanner"
10+
"google.golang.org/api/iterator"
11+
)
12+
13+
// BaselineDateType is an enum representing the type of date to use for baseline counts.
14+
type BaselineDateType string
15+
16+
const (
17+
// BaselineDateTypeLow uses the LowDate from FeatureBaselineStatus.
18+
BaselineDateTypeLow BaselineDateType = "low"
19+
)
20+
21+
// BaselineStatusCountMetric represents a single data point in the baseline status count time series.
22+
type BaselineStatusCountMetric struct {
23+
Date time.Time `spanner:"Date"`
24+
StatusCount int64 `spanner:"StatusCount"`
25+
}
26+
27+
// BaselineStatusCountResultPage is a page of results for the baseline status count query.
28+
type BaselineStatusCountResultPage struct {
29+
NextPageToken *string
30+
Metrics []BaselineStatusCountMetric
31+
}
32+
33+
// baselineStatusCountCursor is used for pagination.
34+
type baselineStatusCountCursor struct {
35+
LastDate time.Time `json:"last_date"`
36+
LastStatusCount int64 `json:"last_status_count"`
37+
}
38+
39+
// decodeBaselineStatusCountCursor decodes a cursor string into a baselineStatusCountCursor.
40+
func decodeBaselineStatusCountCursor(cursor string) (*baselineStatusCountCursor, error) {
41+
return decodeCursor[baselineStatusCountCursor](cursor)
42+
}
43+
44+
// encodeBaselineStatusCountCursor encodes a baselineStatusCountCursor into a cursor string.
45+
func encodeBaselineStatusCountCursor(lastDate time.Time, lastStatusCount int64) string {
46+
return encodeCursor(baselineStatusCountCursor{
47+
LastDate: lastDate,
48+
LastStatusCount: lastStatusCount,
49+
})
50+
}
51+
52+
type fbsColumn string
53+
54+
const fbsColumnLowDate fbsColumn = "fbs.LowDate"
55+
56+
// ListBaselineStatusCount retrieves a cumulative count of baseline features over time.
57+
func (c *Client) ListBaselineStatusCount(
58+
ctx context.Context,
59+
dateType BaselineDateType,
60+
startAt time.Time,
61+
endAt time.Time,
62+
pageSize int,
63+
pageToken *string,
64+
) (*BaselineStatusCountResultPage, error) {
65+
var parsedToken *baselineStatusCountCursor
66+
var err error
67+
if pageToken != nil {
68+
parsedToken, err = decodeBaselineStatusCountCursor(*pageToken)
69+
if err != nil {
70+
return nil, errors.Join(ErrInternalQueryFailure, err)
71+
}
72+
}
73+
74+
txn := c.ReadOnlyTransaction()
75+
defer txn.Close()
76+
77+
// 1. Validate dateType
78+
switch dateType {
79+
case BaselineDateTypeLow:
80+
break
81+
default:
82+
return nil, errors.Join(ErrInternalQueryFailure, fmt.Errorf("invalid BaselineDateType: %s", dateType))
83+
}
84+
85+
// 2. Get excluded feature IDs
86+
excludedFeatureIDs, err := c.getFeatureIDsForEachExcludedFeatureKey(ctx, txn)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
// 3. Calculate initial cumulative count
92+
cumulativeCount, err := c.getInitialBaselineStatusCount(
93+
ctx, txn, parsedToken, startAt, excludedFeatureIDs, dateType)
94+
if err != nil {
95+
return nil, errors.Join(ErrInternalQueryFailure, err)
96+
}
97+
98+
// 4. Process results and update cumulative count
99+
stmt := createListBaselineStatusCountStatement(dateType, startAt, endAt, pageSize, parsedToken, excludedFeatureIDs)
100+
101+
iter := txn.Query(ctx, stmt)
102+
defer iter.Stop()
103+
104+
var metrics []BaselineStatusCountMetric
105+
for {
106+
row, err := iter.Next()
107+
if errors.Is(err, iterator.Done) {
108+
break
109+
}
110+
if err != nil {
111+
return nil, errors.Join(ErrInternalQueryFailure, err)
112+
}
113+
114+
var metric BaselineStatusCountMetric
115+
if err := row.ToStruct(&metric); err != nil {
116+
return nil, err
117+
}
118+
119+
cumulativeCount += metric.StatusCount
120+
metric.StatusCount = cumulativeCount
121+
metrics = append(metrics, metric)
122+
}
123+
124+
var newCursor *string
125+
if len(metrics) == pageSize {
126+
lastMetric := metrics[len(metrics)-1]
127+
generatedCursor := encodeBaselineStatusCountCursor(lastMetric.Date, lastMetric.StatusCount)
128+
newCursor = &generatedCursor
129+
}
130+
131+
return &BaselineStatusCountResultPage{
132+
NextPageToken: newCursor,
133+
Metrics: metrics,
134+
}, nil
135+
}
136+
137+
// getInitialBaselineStatusCount calculates the initial cumulative count for the first page.
138+
func (c *Client) getInitialBaselineStatusCount(
139+
ctx context.Context,
140+
txn *spanner.ReadOnlyTransaction,
141+
parsedToken *baselineStatusCountCursor,
142+
startAt time.Time,
143+
excludedFeatureIDs []string,
144+
dateType BaselineDateType,
145+
) (int64, error) {
146+
if parsedToken != nil {
147+
return parsedToken.LastStatusCount, nil
148+
}
149+
150+
params := map[string]interface{}{
151+
"startAt": startAt,
152+
}
153+
154+
var excludedFeatureFilter string
155+
if len(excludedFeatureIDs) > 0 {
156+
excludedFeatureFilter = `
157+
AND fbs.WebFeatureID NOT IN UNNEST(@excludedFeatureIDs)`
158+
params["excludedFeatureIDs"] = excludedFeatureIDs
159+
}
160+
161+
// Construct the query based on dateType
162+
var dateField string
163+
switch dateType {
164+
case BaselineDateTypeLow:
165+
dateField = string(fbsColumnLowDate)
166+
}
167+
168+
var initialCount int64
169+
stmt := spanner.Statement{
170+
SQL: fmt.Sprintf(`
171+
SELECT COALESCE(SUM(daily_status_count), 0)
172+
FROM (
173+
SELECT COUNT(fbs.WebFeatureID) AS daily_status_count
174+
FROM FeatureBaselineStatus fbs
175+
WHERE %s < @startAt %s
176+
GROUP BY %s
177+
)`, dateField, excludedFeatureFilter, dateField),
178+
Params: params,
179+
}
180+
181+
err := txn.Query(ctx, stmt).Do(func(r *spanner.Row) error {
182+
return r.Column(0, &initialCount)
183+
})
184+
185+
return initialCount, err
186+
}
187+
188+
// createListBaselineStatusCountStatement creates the Spanner statement for the main query.
189+
func createListBaselineStatusCountStatement(
190+
dateType BaselineDateType,
191+
startAt time.Time,
192+
endAt time.Time,
193+
pageSize int,
194+
pageToken *baselineStatusCountCursor,
195+
excludedFeatureIDs []string,
196+
) spanner.Statement {
197+
params := map[string]interface{}{
198+
"startAt": startAt,
199+
"endAt": endAt,
200+
"pageSize": pageSize,
201+
}
202+
203+
var pageFilter string
204+
if pageToken != nil {
205+
var dateField string
206+
switch dateType {
207+
case BaselineDateTypeLow:
208+
dateField = string(fbsColumnLowDate)
209+
}
210+
pageFilter = fmt.Sprintf(`AND %s > @lastDate`, dateField)
211+
params["lastDate"] = pageToken.LastDate
212+
}
213+
214+
var excludedFeatureFilter string
215+
if len(excludedFeatureIDs) > 0 {
216+
excludedFeatureFilter = `AND fbs.WebFeatureID NOT IN UNNEST(@excludedFeatureIDs)`
217+
params["excludedFeatureIDs"] = excludedFeatureIDs
218+
}
219+
220+
// Construct the query based on dateType
221+
var dateField string
222+
switch dateType {
223+
case BaselineDateTypeLow:
224+
dateField = string(fbsColumnLowDate)
225+
}
226+
227+
stmt := spanner.Statement{
228+
SQL: fmt.Sprintf(`
229+
SELECT %s AS Date, COUNT(fbs.WebFeatureID) AS StatusCount
230+
FROM FeatureBaselineStatus fbs
231+
WHERE %s >= @startAt AND %s < @endAt %s %s
232+
GROUP BY %s
233+
ORDER BY %s
234+
LIMIT @pageSize`,
235+
dateField, dateField, dateField, pageFilter, excludedFeatureFilter, dateField, dateField),
236+
Params: params,
237+
}
238+
239+
return stmt
240+
}

0 commit comments

Comments
 (0)