Skip to content

Commit 7a8dfa2

Browse files
committed
Add bcd insert, retreival, tests
1 parent 3dac5da commit 7a8dfa2

File tree

11 files changed

+221
-0
lines changed

11 files changed

+221
-0
lines changed

antlr/FeatureSearch.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ available_date_term:
2929
'available_date' COLON BROWSER_NAME COLON (date_range_query);
3030
// In the future support other operators by doing something like (date_operator_query | date_range_query)
3131
baseline_date_term: 'baseline_date' COLON (date_range_query);
32+
bcd_term: 'bcd' COLON ANY_VALUE;
3233
name_term: 'name' COLON ANY_VALUE;
3334
group_term: 'group' COLON ANY_VALUE;
3435
snapshot_term: 'snapshot' COLON ANY_VALUE;
@@ -38,6 +39,7 @@ term:
3839
| available_on_term
3940
| baseline_status_term
4041
| baseline_date_term
42+
| bcd_term
4143
| group_term
4244
| id_term
4345
| snapshot_term

antlr/FeatureSearch.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ This query language enables you to construct flexible searches to find features
6060
- `name:"Dark Mode"` - Find features named "Dark Mode" (including spaces).
6161
- `baseline_date:2023-01-01..2023-12-31` - Searches for all features that reached baseline in 2023.
6262
- `group:css` - Searches for features that belong to the `css` group and any groups that are descendants of that group.
63+
- `bcd:ToggleEvent` - Searches for features associated with the Browser Compatibility Data key `ToggleEvent`
6364
- `snapshot:ecmascript-5` - Searches for features that belong to the `ecmascript-5` snapshot.
6465
- `id:css` - Searches for a feature whose feature identifier (featurekey) is `css`.
6566

frontend/src/static/js/utils/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ export const VOCABULARY = [
159159
name: 'baseline_status:widely',
160160
doc: 'Features in baseline and widely available',
161161
},
162+
{
163+
name:'bcd:',
164+
doc: 'Features linked to MDN’s Browser Compatibility Data keys. E.g., bcd:ToggleEvent',
165+
},
162166
{
163167
name: 'group:',
164168
doc: 'Features in a group or its descendants. E.g., group:css',

infra/storage/spanner/migrations/000001.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,14 @@ CREATE TABLE IF NOT EXISTS FeatureBaselineStatus (
123123
-- Index to accelerate lookups and joins in FeatureBaselineStatus based on WebFeatureID.
124124
-- Primarily supports queries involving the WebFeatures table.
125125
CREATE INDEX IDX_FBS_FEATUREID ON FeatureBaselineStatus(WebFeatureID);
126+
127+
-- WebFeatureBrowserCompatFeatures stores the compat_features list (e.g. "html.elements.address")
128+
-- for each WebFeature. Multiple compat features may exist per feature.
129+
CREATE TABLE IF NOT EXISTS WebFeatureBrowserCompatFeatures (
130+
WebFeatureID STRING(36) NOT NULL, -- FK to WebFeatures
131+
CompatFeature STRING(255) NOT NULL, -- e.g., "html.elements.address"
132+
FOREIGN KEY (WebFeatureID) REFERENCES WebFeatures(ID)
133+
) PRIMARY KEY (WebFeatureID, CompatFeature);
134+
135+
-- Index to accelerate searches by CompatFeature
136+
CREATE INDEX IDX_CompatFeature ON WebFeatureBrowserCompatFeatures(CompatFeature);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package gcpspanner
2+
3+
import (
4+
"context"
5+
6+
"cloud.google.com/go/spanner"
7+
)
8+
9+
func (c *Client) UpsertBrowserCompatFeatures(ctx context.Context, featureID string, compatFeatures []string) error {
10+
var muts []*spanner.Mutation
11+
for _, compat := range compatFeatures {
12+
muts = append(muts, spanner.InsertOrUpdate("WebFeatureBrowserCompatFeatures", []string{
13+
"WebFeatureID", "CompatFeature",
14+
}, []interface{}{featureID, compat}))
15+
}
16+
_, err := c.Apply(ctx, muts)
17+
return err
18+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
"slices"
20+
"testing"
21+
22+
"cloud.google.com/go/spanner"
23+
"google.golang.org/api/iterator"
24+
)
25+
26+
func TestUpsertBrowserCompatFeatures(t *testing.T) {
27+
restartDatabaseContainer(t)
28+
ctx := context.Background()
29+
30+
feature := getSampleFeatures()[0]
31+
featureID, err := spannerClient.UpsertWebFeature(ctx, feature)
32+
if err != nil {
33+
t.Fatalf("failed to insert feature: %v", err)
34+
}
35+
36+
initial := []string{"html.elements.address", "html.elements.section"}
37+
err = spannerClient.UpsertBrowserCompatFeatures(ctx, *featureID, initial)
38+
if err != nil {
39+
t.Fatalf("UpsertBrowserCompatFeatures initial insert failed: %v", err)
40+
}
41+
42+
expected := slices.Clone(initial)
43+
details := readAllBrowserCompatFeatures(t, ctx, *featureID)
44+
slices.Sort(details)
45+
slices.Sort(expected)
46+
if !slices.Equal(details, expected) {
47+
t.Errorf("initial compat features mismatch.\nexpected %+v\nreceived %+v", expected, details)
48+
}
49+
50+
updated := []string{"html.elements.article"}
51+
err = spannerClient.UpsertBrowserCompatFeatures(ctx, *featureID, updated)
52+
if err != nil {
53+
t.Fatalf("UpsertBrowserCompatFeatures update failed: %v", err)
54+
}
55+
56+
expected = slices.Clone(updated)
57+
details = readAllBrowserCompatFeatures(t, ctx, *featureID)
58+
slices.Sort(details)
59+
slices.Sort(expected)
60+
if !slices.Equal(details, expected) {
61+
t.Errorf("updated compat features mismatch.\nexpected %+v\nreceived %+v", expected, details)
62+
}
63+
}
64+
65+
func readAllBrowserCompatFeatures(t *testing.T, ctx context.Context, featureID string) []string {
66+
stmt := spanner.NewStatement(`
67+
SELECT CompatFeature
68+
FROM WebFeatureBrowserCompatFeatures
69+
WHERE WebFeatureID = @id`)
70+
stmt.Params["id"] = featureID
71+
72+
iter := spannerClient.Single().Query(ctx, stmt)
73+
defer iter.Stop()
74+
75+
var features []string
76+
for {
77+
row, err := iter.Next()
78+
if err == iterator.Done {
79+
break
80+
}
81+
if err != nil {
82+
t.Fatalf("query failed: %v", err)
83+
}
84+
var compat string
85+
if err := row.Columns(&compat); err != nil {
86+
t.Fatalf("column parse failed: %v", err)
87+
}
88+
features = append(features, compat)
89+
}
90+
91+
return features
92+
}

lib/gcpspanner/feature_search_query.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ func (b *FeatureSearchFilterBuilder) traverseAndGenerateFilters(node *searchtype
147147
filter = b.baselineDateFilter(node.Term.Value, node.Term.Operator)
148148
case searchtypes.IdentifierAvailableBrowserDate:
149149
filter = b.handleIdentifierAvailableBrowserDateTerm(node)
150+
case searchtypes.IdentifierBrowserCompatData:
151+
filter = b.browserCompatDataFilter(node.Term.Value, node.Term.Operator)
150152
}
151153
if filter != "" {
152154
filters = append(filters, filter)
@@ -393,6 +395,18 @@ func (b *FeatureSearchFilterBuilder) baselineDateFilter(rawDate string, op searc
393395
return fmt.Sprintf(`LowDate %s @%s`, searchOperatorToSpannerBinaryOperator(op), paramName)
394396
}
395397

398+
func (b *FeatureSearchFilterBuilder) browserCompatDataFilter(bcdKey string, op searchtypes.SearchOperator) string {
399+
paramName := b.addParamGetName(bcdKey)
400+
401+
return fmt.Sprintf(`
402+
wf.ID IN (
403+
SELECT WebFeatureID
404+
FROM WebFeatureBrowserCompatFeatures
405+
WHERE CompatFeature %s @%s
406+
)
407+
`, searchOperatorToSpannerBinaryOperator(op), paramName)
408+
}
409+
396410
// Exclude all that do not have an entry in ExcludedFeatureKeys.
397411
const removeExcludedKeyFilter = "efk.FeatureKey IS NULL"
398412
const removeExcludedKeyFilterAND = "AND " + removeExcludedKeyFilter

lib/gcpspanner/searchtypes/features_search_visitor.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ func (v *FeaturesSearchVisitor) createNameNode(nameNode antlr.TerminalNode) *Sea
160160
return v.createSimpleNode(nameNode, IdentifierName)
161161
}
162162

163+
func (v *FeaturesSearchVisitor) createBrowserCompatDataNode(bcdNode antlr.TerminalNode) *SearchNode {
164+
return v.createSimpleNode(bcdNode, IdentifierBrowserCompatData)
165+
}
166+
163167
func (v *FeaturesSearchVisitor) createSimpleNode(
164168
node antlr.TerminalNode,
165169
identifier SearchIdentifier) *SearchNode {
@@ -512,6 +516,11 @@ func (v *FeaturesSearchVisitor) VisitName_term(ctx *parser.Name_termContext) int
512516
return v.createNameNode(ctx.ANY_VALUE())
513517
}
514518

519+
// nolint: revive // Method signature is generated.
520+
func (v *FeaturesSearchVisitor) VisitBcd_term(ctx *parser.Bcd_termContext) interface{} {
521+
return v.createBrowserCompatDataNode(ctx.ANY_VALUE())
522+
}
523+
515524
func (v *FeaturesSearchVisitor) VisitTerm(ctx *parser.TermContext) interface{} {
516525
return v.VisitChildren(ctx)
517526
}

lib/gcpspanner/searchtypes/searchtypes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const (
9292
IdentifierAvailableOn SearchIdentifier = "available_on"
9393
IdentifierBaselineDate SearchIdentifier = "baseline_date"
9494
IdentifierBaselineStatus SearchIdentifier = "baseline_status"
95+
IdentifierBrowserCompatData SearchIdentifier = "bcd"
9596
IdentifierName SearchIdentifier = "name"
9697
IdentifierGroup SearchIdentifier = "group"
9798
IdentifierSnapshot SearchIdentifier = "snapshot"

lib/gcpspanner/spanneradapters/web_features_consumer.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type WebFeatureSpannerClient interface {
3535
UpsertFeatureDiscouragedDetails(ctx context.Context, featureID string,
3636
in gcpspanner.FeatureDiscouragedDetails) error
3737
PrecalculateBrowserFeatureSupportEvents(ctx context.Context, startAt, endAt time.Time) error
38+
UpsertBrowserCompatFeatures(ctx context.Context, featureID string, compatFeatures []string) error
3839
}
3940

4041
// NewWebFeaturesConsumer constructs an adapter for the web features consumer service.
@@ -99,6 +100,14 @@ func (c *WebFeaturesConsumer) InsertWebFeatures(
99100
return nil, err
100101
}
101102

103+
if len(featureData.CompatFeatures) > 0 {
104+
err = c.client.UpsertBrowserCompatFeatures(ctx, *id, featureData.CompatFeatures)
105+
if err != nil {
106+
slog.ErrorContext(ctx, "unable to insert compat features", "featureID", *id, "error", err)
107+
return nil, err
108+
}
109+
}
110+
102111
if featureData.Discouraged != nil {
103112
err = c.client.UpsertFeatureDiscouragedDetails(ctx, featureID, gcpspanner.FeatureDiscouragedDetails{
104113
AccordingTo: featureData.Discouraged.AccordingTo,

lib/gcpspanner/spanneradapters/web_features_consumer_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@ type mockUpsertFeatureDiscouragedDetailsConfig struct {
229229
expectedCount int
230230
}
231231

232+
type mockUpsertBrowserCompatFeaturesConfig struct {
233+
expectedInputs map[string][]string
234+
outputs map[string]error
235+
expectedCount int
236+
}
237+
232238
type mockWebFeatureSpannerClient struct {
233239
t *testing.T
234240
upsertWebFeatureCount int
@@ -243,6 +249,8 @@ type mockWebFeatureSpannerClient struct {
243249
precalculateBrowserFeatureSupportEventsCount int
244250
mockUpsertFeatureDiscouragedDetailsCfg mockUpsertFeatureDiscouragedDetailsConfig
245251
upsertFeatureDiscouragedDetailsCount int
252+
mockUpsertBrowserCompatFeaturesCfg mockUpsertBrowserCompatFeaturesConfig
253+
upsertBrowserCompatFeaturesCount int
246254
}
247255

248256
func (c *mockWebFeatureSpannerClient) UpsertWebFeature(
@@ -367,6 +375,22 @@ func (c *mockWebFeatureSpannerClient) UpsertFeatureDiscouragedDetails(
367375
return c.mockUpsertFeatureDiscouragedDetailsCfg.outputs[featureID]
368376
}
369377

378+
func (c *mockWebFeatureSpannerClient) UpsertBrowserCompatFeatures(
379+
_ context.Context, featureID string, compatFeatures []string) error {
380+
if len(c.mockUpsertBrowserCompatFeaturesCfg.expectedInputs) <= c.upsertBrowserCompatFeaturesCount {
381+
c.t.Fatal("no more expected input for UpsertBrowserCompatFeatures")
382+
}
383+
expectedInput, found := c.mockUpsertBrowserCompatFeaturesCfg.expectedInputs[featureID]
384+
if !found {
385+
c.t.Errorf("unexpected input for featureID %v", featureID)
386+
}
387+
if !reflect.DeepEqual(expectedInput, compatFeatures) {
388+
c.t.Errorf("unexpected input expected %v received %v", expectedInput, compatFeatures)
389+
}
390+
c.upsertBrowserCompatFeaturesCount++
391+
return c.mockUpsertBrowserCompatFeaturesCfg.outputs[featureID]
392+
}
393+
370394
func newMockmockWebFeatureSpannerClient(
371395
t *testing.T,
372396
mockUpsertWebFeatureCfg mockUpsertWebFeatureConfig,
@@ -375,6 +399,7 @@ func newMockmockWebFeatureSpannerClient(
375399
mockUpsertFeatureSpecCfg mockUpsertFeatureSpecConfig,
376400
mocmockPrecalculateBrowserFeatureSupportEventsCfg mockPrecalculateBrowserFeatureSupportEventsConfig,
377401
mockUpsertFeatureDiscouragedDetailsCfg mockUpsertFeatureDiscouragedDetailsConfig,
402+
mockUpsertBrowserCompatFeaturesCfg mockUpsertBrowserCompatFeaturesConfig,
378403
) *mockWebFeatureSpannerClient {
379404
return &mockWebFeatureSpannerClient{
380405
t: t,
@@ -390,6 +415,8 @@ func newMockmockWebFeatureSpannerClient(
390415
precalculateBrowserFeatureSupportEventsCount: 0,
391416
mockUpsertFeatureDiscouragedDetailsCfg: mockUpsertFeatureDiscouragedDetailsCfg,
392417
upsertFeatureDiscouragedDetailsCount: 0,
418+
mockUpsertBrowserCompatFeaturesCfg: mockUpsertBrowserCompatFeaturesCfg,
419+
upsertBrowserCompatFeaturesCount: 0,
393420
}
394421
}
395422

@@ -416,6 +443,7 @@ func TestInsertWebFeatures(t *testing.T) {
416443
mockUpsertFeatureSpecCfg mockUpsertFeatureSpecConfig
417444
mockPrecalculateBrowserFeatureSupportEventsCfg mockPrecalculateBrowserFeatureSupportEventsConfig
418445
mockUpsertFeatureDiscouragedDetailsCfg mockUpsertFeatureDiscouragedDetailsConfig
446+
mockUpsertBrowserCompatFeaturesConfig mockUpsertBrowserCompatFeaturesConfig
419447
input map[string]web_platform_dx__web_features.FeatureValue
420448
expectedError error // Expected error from InsertWebFeatures
421449
}{
@@ -612,6 +640,15 @@ func TestInsertWebFeatures(t *testing.T) {
612640
outputs: map[string]error{"feature1": nil},
613641
expectedCount: 1,
614642
},
643+
mockUpsertBrowserCompatFeaturesConfig: mockUpsertBrowserCompatFeaturesConfig{
644+
expectedInputs: map[string][]string{
645+
"feature1": {"html.elements.address", "html.elements.section"},
646+
},
647+
outputs: map[string]error{
648+
"feature1": nil,
649+
},
650+
expectedCount: 1,
651+
},
615652
expectedError: nil,
616653
},
617654
{
@@ -655,6 +692,11 @@ func TestInsertWebFeatures(t *testing.T) {
655692
outputs: map[string]error{},
656693
expectedCount: 0,
657694
},
695+
mockUpsertBrowserCompatFeaturesConfig: mockUpsertBrowserCompatFeaturesConfig{
696+
expectedInputs: map[string][]string{},
697+
outputs: map[string]error{},
698+
expectedCount: 0,
699+
},
658700
input: map[string]web_platform_dx__web_features.FeatureValue{
659701
"feature1": {
660702
Name: "Feature 1",
@@ -728,6 +770,11 @@ func TestInsertWebFeatures(t *testing.T) {
728770
outputs: map[string]error{},
729771
expectedCount: 0,
730772
},
773+
mockUpsertBrowserCompatFeaturesConfig: mockUpsertBrowserCompatFeaturesConfig{
774+
expectedInputs: map[string][]string{},
775+
outputs: map[string]error{},
776+
expectedCount: 0,
777+
},
731778
input: map[string]web_platform_dx__web_features.FeatureValue{
732779
"feature1": {
733780
Name: "Feature 1",
@@ -861,6 +908,11 @@ func TestInsertWebFeatures(t *testing.T) {
861908
outputs: map[string]error{},
862909
expectedCount: 0,
863910
},
911+
mockUpsertBrowserCompatFeaturesConfig: mockUpsertBrowserCompatFeaturesConfig{
912+
expectedInputs: map[string][]string{},
913+
outputs: map[string]error{},
914+
expectedCount: 0,
915+
},
864916
expectedError: ErrBrowserFeatureAvailabilityTest,
865917
},
866918
{
@@ -1191,6 +1243,7 @@ func TestInsertWebFeatures(t *testing.T) {
11911243
tc.mockUpsertFeatureSpecCfg,
11921244
tc.mockPrecalculateBrowserFeatureSupportEventsCfg,
11931245
tc.mockUpsertFeatureDiscouragedDetailsCfg,
1246+
tc.mockUpsertBrowserCompatFeaturesConfig,
11941247
)
11951248
consumer := NewWebFeaturesConsumer(mockClient)
11961249

@@ -1241,6 +1294,13 @@ func TestInsertWebFeatures(t *testing.T) {
12411294
mockClient.mockUpsertFeatureDiscouragedDetailsCfg.expectedCount,
12421295
mockClient.upsertFeatureDiscouragedDetailsCount)
12431296
}
1297+
1298+
if mockClient.upsertBrowserCompatFeaturesCount !=
1299+
tc.mockUpsertBrowserCompatFeaturesConfig.expectedCount {
1300+
t.Errorf("expected %d calls to UpsertBrowserCompatFeatures, got %d",
1301+
tc.mockUpsertBrowserCompatFeaturesConfig.expectedCount,
1302+
mockClient.upsertBrowserCompatFeaturesCount)
1303+
}
12441304
})
12451305
}
12461306
}

0 commit comments

Comments
 (0)