Skip to content

Commit 966bfff

Browse files
authored
Spanner query for missing one implementation feature IDs (#1265)
* Implement Spanner query to retrieve feature IDs for missing one implementation * Lint fix * Address review comments
1 parent a32dce6 commit 966bfff

File tree

3 files changed

+398
-0
lines changed

3 files changed

+398
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package gcpspanner
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"time"
22+
23+
"cloud.google.com/go/spanner"
24+
"google.golang.org/api/iterator"
25+
)
26+
27+
func init() {
28+
missingOneImplFeatureListTemplate = NewQueryTemplate(missingOneImplFeatureListRawTemplate)
29+
}
30+
31+
// nolint: gochecknoglobals // WONTFIX. Compile the template once at startup. Startup fails if invalid.
32+
var (
33+
// missingOneImplFeatureListTemplate is the compiled version of missingOneImplFeatureListRawTemplate.
34+
missingOneImplFeatureListTemplate BaseQueryTemplate
35+
)
36+
37+
// MissingOneImplFeatureListPage contains the details for the missing one implementation feature list request.
38+
type MissingOneImplFeatureListPage struct {
39+
NextPageToken *string
40+
FeatureList []MissingOneImplFeature
41+
}
42+
43+
// MissingOneImplFeature contains information regarding the list of features implemented in all other browsers but not
44+
// in the target browser.
45+
type MissingOneImplFeature struct {
46+
WebFeatureID string `spanner:"KEY"`
47+
}
48+
49+
const missingOneImplFeatureListRawTemplate = `
50+
SELECT wf.FeatureKey as KEY
51+
FROM WebFeatures wf
52+
WHERE wf.ID IN (
53+
SELECT bfse.WebFeatureID
54+
FROM BrowserFeatureSupportEvents bfse
55+
WHERE bfse.TargetBrowserName = @targetBrowserParam
56+
AND bfse.EventReleaseDate = @targetDate
57+
AND bfse.SupportStatus = 'unsupported'
58+
)
59+
AND {{ range $browserParamName := .OtherBrowsersParamNames }}
60+
EXISTS (
61+
SELECT 1
62+
FROM BrowserFeatureSupportEvents bfse_other
63+
WHERE bfse_other.WebFeatureID = wf.ID
64+
AND bfse_other.TargetBrowserName = @{{ $browserParamName }}
65+
AND bfse_other.EventReleaseDate = @targetDate
66+
AND bfse_other.SupportStatus = 'supported'
67+
)
68+
AND
69+
{{ end }}
70+
1=1
71+
ORDER BY KEY ASC
72+
`
73+
74+
type missingOneImplFeatureListTemplateData struct {
75+
OtherBrowsersParamNames []string
76+
}
77+
78+
func buildMissingOneImplFeatureListTemplate(
79+
targetBrowser string,
80+
otherBrowsers []string,
81+
targetDate time.Time,
82+
) spanner.Statement {
83+
params := map[string]interface{}{}
84+
allBrowsers := make([]string, len(otherBrowsers)+1)
85+
copy(allBrowsers, otherBrowsers)
86+
allBrowsers[len(allBrowsers)-1] = targetBrowser
87+
params["targetBrowserParam"] = targetBrowser
88+
otherBrowsersParamNames := make([]string, 0, len(otherBrowsers))
89+
for i := range otherBrowsers {
90+
paramName := fmt.Sprintf("otherBrowser%d", i)
91+
params[paramName] = otherBrowsers[i]
92+
otherBrowsersParamNames = append(otherBrowsersParamNames, paramName)
93+
}
94+
95+
params["targetDate"] = targetDate
96+
97+
tmplData := missingOneImplFeatureListTemplateData{
98+
OtherBrowsersParamNames: otherBrowsersParamNames,
99+
}
100+
101+
sql := missingOneImplFeatureListTemplate.Execute(tmplData)
102+
stmt := spanner.NewStatement(sql)
103+
stmt.Params = params
104+
105+
return stmt
106+
}
107+
108+
func (c *Client) MissingOneImplFeatureList(
109+
ctx context.Context,
110+
targetBrowser string,
111+
otherBrowsers []string,
112+
targetDate time.Time,
113+
) (*MissingOneImplFeatureListPage, error) {
114+
txn := c.ReadOnlyTransaction()
115+
defer txn.Close()
116+
117+
stmt := buildMissingOneImplFeatureListTemplate(
118+
targetBrowser,
119+
otherBrowsers,
120+
targetDate,
121+
)
122+
123+
it := txn.Query(ctx, stmt)
124+
defer it.Stop()
125+
126+
var results []MissingOneImplFeature
127+
for {
128+
row, err := it.Next()
129+
if errors.Is(err, iterator.Done) {
130+
break
131+
}
132+
if err != nil {
133+
return nil, err
134+
}
135+
var result MissingOneImplFeature
136+
if err := row.ToStruct(&result); err != nil {
137+
return nil, err
138+
}
139+
results = append(results, MissingOneImplFeature{result.WebFeatureID})
140+
}
141+
142+
page := MissingOneImplFeatureListPage{
143+
FeatureList: results,
144+
NextPageToken: nil,
145+
}
146+
147+
return &page, nil
148+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package gcpspanner
16+
17+
import (
18+
"context"
19+
"reflect"
20+
"slices"
21+
"testing"
22+
"time"
23+
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
// nolint:dupl // WONTFIX
28+
func loadDataForListMissingOneImplFeatureList(ctx context.Context, t *testing.T, client *Client) {
29+
webFeatures := []WebFeature{
30+
{FeatureKey: "FeatureX", Name: "Cool API"},
31+
{FeatureKey: "FeatureY", Name: "Super API"},
32+
{FeatureKey: "FeatureZ", Name: "Neat API"},
33+
{FeatureKey: "FeatureW", Name: "Amazing API"},
34+
}
35+
for _, feature := range webFeatures {
36+
_, err := client.UpsertWebFeature(ctx, feature)
37+
if err != nil {
38+
t.Errorf("unexpected error during insert of features. %s", err.Error())
39+
}
40+
}
41+
42+
browserReleases := []BrowserRelease{
43+
// fooBrowser Releases
44+
{BrowserName: "fooBrowser", BrowserVersion: "110", ReleaseDate: time.Date(2024, 1, 10, 0, 0, 0, 0, time.UTC)},
45+
{BrowserName: "fooBrowser", BrowserVersion: "111", ReleaseDate: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC)},
46+
{BrowserName: "fooBrowser", BrowserVersion: "112", ReleaseDate: time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)},
47+
{BrowserName: "fooBrowser", BrowserVersion: "113", ReleaseDate: time.Date(2024, 4, 15, 0, 0, 0, 0, time.UTC)},
48+
49+
// barBrowser Releases
50+
{BrowserName: "barBrowser", BrowserVersion: "113", ReleaseDate: time.Date(2024, 1, 20, 0, 0, 0, 0, time.UTC)},
51+
{BrowserName: "barBrowser", BrowserVersion: "114", ReleaseDate: time.Date(2024, 3, 28, 0, 0, 0, 0, time.UTC)},
52+
{BrowserName: "barBrowser", BrowserVersion: "115", ReleaseDate: time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC)},
53+
54+
// bazBrowser Releases
55+
{BrowserName: "bazBrowser", BrowserVersion: "16.4", ReleaseDate: time.Date(2024, 1, 25, 0, 0, 0, 0, time.UTC)},
56+
{BrowserName: "bazBrowser", BrowserVersion: "16.5", ReleaseDate: time.Date(2024, 3, 5, 0, 0, 0, 0, time.UTC)},
57+
{BrowserName: "bazBrowser", BrowserVersion: "17", ReleaseDate: time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC)},
58+
}
59+
for _, release := range browserReleases {
60+
err := client.InsertBrowserRelease(ctx, release)
61+
if err != nil {
62+
t.Errorf("unexpected error during insert of releases. %s", err.Error())
63+
}
64+
}
65+
66+
browserFeatureAvailabilities := []struct {
67+
FeatureKey string
68+
BrowserFeatureAvailability
69+
}{
70+
// fooBrowser Availabilities
71+
{
72+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "fooBrowser", BrowserVersion: "111"},
73+
FeatureKey: "FeatureX",
74+
}, // Available from fooBrowser 111
75+
{
76+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "fooBrowser", BrowserVersion: "112"},
77+
FeatureKey: "FeatureY",
78+
}, // Available from fooBrowser 112
79+
{
80+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "fooBrowser", BrowserVersion: "112"},
81+
FeatureKey: "FeatureZ",
82+
}, // Available from fooBrowser 112
83+
{
84+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "fooBrowser", BrowserVersion: "113"},
85+
FeatureKey: "FeatureW",
86+
}, // Available from fooBrowser 113
87+
88+
// barBrowser Availabilities
89+
{
90+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "barBrowser", BrowserVersion: "113"},
91+
FeatureKey: "FeatureX",
92+
}, // Available from barBrowser 113
93+
{
94+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "barBrowser", BrowserVersion: "113"},
95+
FeatureKey: "FeatureZ",
96+
}, // Available from barBrowser 113
97+
{
98+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "barBrowser", BrowserVersion: "114"},
99+
FeatureKey: "FeatureY",
100+
}, // Available from barBrowser 114
101+
{
102+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "barBrowser", BrowserVersion: "115"},
103+
FeatureKey: "FeatureW",
104+
}, // Available from barBrowser 115
105+
106+
// bazBrowser Availabilities
107+
{
108+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "bazBrowser", BrowserVersion: "16.4"},
109+
FeatureKey: "FeatureX",
110+
}, // Available from bazBrowser 16.4
111+
{
112+
BrowserFeatureAvailability: BrowserFeatureAvailability{BrowserName: "bazBrowser", BrowserVersion: "16.5"},
113+
FeatureKey: "FeatureY",
114+
}, // Available from bazBrowser 16.5
115+
}
116+
for _, availability := range browserFeatureAvailabilities {
117+
err := client.InsertBrowserFeatureAvailability(ctx,
118+
availability.FeatureKey, availability.BrowserFeatureAvailability)
119+
if err != nil {
120+
t.Errorf("unexpected error during insert. %s", err.Error())
121+
}
122+
}
123+
err := spannerClient.PrecalculateBrowserFeatureSupportEvents(ctx,
124+
time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
125+
if err != nil {
126+
t.Errorf("unexpected error during pre-calculate. %s", err.Error())
127+
}
128+
}
129+
130+
func assertMissingOneImplFeatureList(ctx context.Context, t *testing.T, targetDate time.Time,
131+
targetBrowser string, otherBrowsers []string, expectedPage *MissingOneImplFeatureListPage) {
132+
result, err := spannerClient.MissingOneImplFeatureList(
133+
ctx,
134+
targetBrowser,
135+
otherBrowsers,
136+
targetDate,
137+
)
138+
if err != nil {
139+
t.Errorf("Unexpected error: %v", err)
140+
}
141+
if !reflect.DeepEqual(expectedPage.NextPageToken, result.NextPageToken) {
142+
t.Errorf("unexpected result.\nExpected %+v\nReceived %+v", expectedPage, result)
143+
}
144+
if !assert.ElementsMatch(t, expectedPage.FeatureList, result.FeatureList) {
145+
t.Errorf("unexpected result.\nExpected %+v\nReceived %+v", expectedPage, result)
146+
}
147+
}
148+
149+
func testMissingOneImplFeatureListSuite(
150+
ctx context.Context,
151+
t *testing.T,
152+
) {
153+
t.Run("bazBrowser", func(t *testing.T) {
154+
const targetBrowser = "bazBrowser"
155+
otherBrowsers := []string{
156+
"fooBrowser",
157+
"barBrowser",
158+
}
159+
targetDate := time.Date(2024, 4, 15, 0, 0, 0, 0, time.UTC)
160+
161+
t.Run("simple successful query", func(t *testing.T) {
162+
expectedResult := &MissingOneImplFeatureListPage{
163+
NextPageToken: nil,
164+
FeatureList: []MissingOneImplFeature{
165+
// fooBrowser 113 release
166+
// Currently supported features:
167+
// fooBrowser: FeatureX, FeatureZ, FeatureY, FeatureW
168+
// barBrowser: FeatureX, FeatureZ, FeatureY, FeatureW
169+
// bazBrowser: FeatureX, FeatureY
170+
// Missing in on for bazBrowser: FeatureW, FeatureZ
171+
{
172+
WebFeatureID: "FeatureZ",
173+
},
174+
{
175+
WebFeatureID: "FeatureW",
176+
},
177+
},
178+
}
179+
assertMissingOneImplFeatureList(
180+
ctx,
181+
t,
182+
targetDate,
183+
targetBrowser,
184+
otherBrowsers,
185+
expectedResult,
186+
)
187+
})
188+
189+
t.Run("empty query result", func(t *testing.T) {
190+
emptyDate := time.Date(2024, 3, 5, 0, 0, 0, 0, time.UTC)
191+
expectedResult := &MissingOneImplFeatureListPage{
192+
NextPageToken: nil,
193+
FeatureList: []MissingOneImplFeature{},
194+
}
195+
assertMissingOneImplFeatureList(
196+
ctx,
197+
t,
198+
emptyDate,
199+
targetBrowser,
200+
otherBrowsers,
201+
expectedResult,
202+
)
203+
})
204+
205+
t.Run("simple query at a smaller subset of otherBrowsers", func(t *testing.T) {
206+
subsetBrowsers := []string{
207+
"barBrowser",
208+
}
209+
210+
expectedResult := &MissingOneImplFeatureListPage{
211+
NextPageToken: nil,
212+
FeatureList: []MissingOneImplFeature{
213+
// fooBrowser 113 release
214+
// Currently supported features:
215+
// fooBrowser: FeatureX, FeatureZ, FeatureY, FeatureW
216+
// barBrowser: FeatureX, FeatureZ, FeatureY, FeatureW
217+
// bazBrowser: FeatureX, FeatureY
218+
// Missing in on for bazBrowser: FeatureW, FeatureZ
219+
{
220+
WebFeatureID: "FeatureZ",
221+
},
222+
{
223+
WebFeatureID: "FeatureW",
224+
},
225+
},
226+
}
227+
assertMissingOneImplFeatureList(
228+
ctx,
229+
t,
230+
targetDate,
231+
targetBrowser,
232+
subsetBrowsers,
233+
expectedResult,
234+
)
235+
})
236+
})
237+
}
238+
239+
func TestListMissingOneImplFeatureList(t *testing.T) {
240+
restartDatabaseContainer(t)
241+
ctx := context.Background()
242+
243+
loadDataForListMissingOneImplFeatureList(ctx, t, spannerClient)
244+
actualEvents := spannerClient.readAllBrowserFeatureSupportEvents(ctx, t)
245+
slices.SortFunc(actualEvents, sortBrowserFeatureSupportEvents)
246+
t.Run("MissingOneImplFeatureListQuery", func(t *testing.T) {
247+
testMissingOneImplFeatureListSuite(ctx, t)
248+
})
249+
}

0 commit comments

Comments
 (0)