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,

0 commit comments

Comments
 (0)