Skip to content

Add bcd key insert, retreival, tests #1469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions antlr/FeatureSearch.g4
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ available_date_term:
'available_date' COLON BROWSER_NAME COLON (date_range_query);
// In the future support other operators by doing something like (date_operator_query | date_range_query)
baseline_date_term: 'baseline_date' COLON (date_range_query);
bcd_term: 'bcd' COLON ANY_VALUE;
name_term: 'name' COLON ANY_VALUE;
group_term: 'group' COLON ANY_VALUE;
snapshot_term: 'snapshot' COLON ANY_VALUE;
Expand All @@ -38,6 +39,7 @@ term:
| available_on_term
| baseline_status_term
| baseline_date_term
| bcd_term
| group_term
| id_term
| snapshot_term
Expand Down
1 change: 1 addition & 0 deletions antlr/FeatureSearch.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ This query language enables you to construct flexible searches to find features
- `name:"Dark Mode"` - Find features named "Dark Mode" (including spaces).
- `baseline_date:2023-01-01..2023-12-31` - Searches for all features that reached baseline in 2023.
- `group:css` - Searches for features that belong to the `css` group and any groups that are descendants of that group.
- `bcd:ToggleEvent` - Searches for features associated with the Browser Compatibility Data key `ToggleEvent`
- `snapshot:ecmascript-5` - Searches for features that belong to the `ecmascript-5` snapshot.
- `id:css` - Searches for a feature whose feature identifier (featurekey) is `css`.

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/static/js/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ export const VOCABULARY = [
name: 'baseline_status:widely',
doc: 'Features in baseline and widely available',
},
{
name: 'bcd:',
doc: 'Features linked to MDN’s Browser Compatibility Data keys. E.g., bcd:ToggleEvent',
},
{
name: 'group:',
doc: 'Features in a group or its descendants. E.g., group:css',
Expand Down
25 changes: 25 additions & 0 deletions infra/storage/spanner/migrations/000016.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Copyright 2025 Google LLC
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

-- WebFeatureBrowserCompatFeatures stores the compat_features list (e.g. "html.elements.address")
-- for each WebFeature. Multiple compat features may exist per feature.
CREATE TABLE IF NOT EXISTS WebFeatureBrowserCompatFeatures (
ID STRING(36) NOT NULL, -- same name and type as parent
CompatFeature STRING(255) NOT NULL,
FOREIGN KEY (ID) REFERENCES WebFeatures(ID)
) PRIMARY KEY (ID, CompatFeature)
, INTERLEAVE IN PARENT WebFeatures ON DELETE CASCADE;

-- Index to accelerate searches by CompatFeature
CREATE INDEX IDX_CompatFeature ON WebFeatureBrowserCompatFeatures(CompatFeature);
38 changes: 38 additions & 0 deletions lib/gcpspanner/feature_browser_compat_features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcpspanner

import (
"context"

"cloud.google.com/go/spanner"
)

func (c *Client) UpsertBrowserCompatFeatures(ctx context.Context, featureID string, compatFeatures []string) error {
// Create a delete mutation for the specified KeyRange
del := spanner.Delete("WebFeatureBrowserCompatFeatures", spanner.Key{featureID}.AsPrefix())

// Then, insert new ones
muts := make([]*spanner.Mutation, 0, len(compatFeatures)+1)
muts = append(muts, del)
for _, compat := range compatFeatures {
muts = append(muts, spanner.InsertOrUpdate("WebFeatureBrowserCompatFeatures", []string{
"ID", "CompatFeature",
}, []interface{}{featureID, compat}))
}
_, err := c.Apply(ctx, muts)

return err
}
93 changes: 93 additions & 0 deletions lib/gcpspanner/feature_browser_compat_features_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcpspanner

import (
"context"
"errors"
"slices"
"testing"

"cloud.google.com/go/spanner"
"google.golang.org/api/iterator"
)

func TestUpsertBrowserCompatFeatures(t *testing.T) {
restartDatabaseContainer(t)
ctx := context.Background()

feature := getSampleFeatures()[0]
featureID, err := spannerClient.UpsertWebFeature(ctx, feature)
if err != nil {
t.Fatalf("failed to insert feature: %v", err)
}

initial := []string{"html.elements.address", "html.elements.section"}
err = spannerClient.UpsertBrowserCompatFeatures(ctx, *featureID, initial)
if err != nil {
t.Fatalf("UpsertBrowserCompatFeatures initial insert failed: %v", err)
}

expected := slices.Clone(initial)
details := readAllBrowserCompatFeatures(ctx, t, *featureID)
slices.Sort(details)
slices.Sort(expected)
if !slices.Equal(details, expected) {
t.Errorf("initial compat features mismatch.\nexpected %+v\nreceived %+v", expected, details)
}

updated := []string{"html.elements.article"}
err = spannerClient.UpsertBrowserCompatFeatures(ctx, *featureID, updated)
if err != nil {
t.Fatalf("UpsertBrowserCompatFeatures update failed: %v", err)
}

expected = slices.Clone(updated)
details = readAllBrowserCompatFeatures(ctx, t, *featureID)
slices.Sort(details)
slices.Sort(expected)
if !slices.Equal(details, expected) {
t.Errorf("updated compat features mismatch.\nexpected %+v\nreceived %+v", expected, details)
}
}

func readAllBrowserCompatFeatures(ctx context.Context, t *testing.T, featureID string) []string {
stmt := spanner.NewStatement(`
SELECT CompatFeature
FROM WebFeatureBrowserCompatFeatures
WHERE ID = @id`)
stmt.Params["id"] = featureID

iter := spannerClient.Single().Query(ctx, stmt)
defer iter.Stop()

var features []string
for {
row, err := iter.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
t.Fatalf("query failed: %v", err)
}
var compat string
if err := row.Columns(&compat); err != nil {
t.Fatalf("column parse failed: %v", err)
}
features = append(features, compat)
}

return features
}
9 changes: 9 additions & 0 deletions lib/gcpspanner/feature_search_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ func (b *FeatureSearchFilterBuilder) traverseAndGenerateFilters(node *searchtype
filter = b.baselineDateFilter(node.Term.Value, node.Term.Operator)
case searchtypes.IdentifierAvailableBrowserDate:
filter = b.handleIdentifierAvailableBrowserDateTerm(node)
case searchtypes.IdentifierBrowserCompatData:
filter = b.browserCompatDataFilter(node.Term.Value, node.Term.Operator)
}
if filter != "" {
filters = append(filters, filter)
Expand Down Expand Up @@ -393,6 +395,13 @@ func (b *FeatureSearchFilterBuilder) baselineDateFilter(rawDate string, op searc
return fmt.Sprintf(`LowDate %s @%s`, searchOperatorToSpannerBinaryOperator(op), paramName)
}

func (b *FeatureSearchFilterBuilder) browserCompatDataFilter(bcdKey string, op searchtypes.SearchOperator) string {
paramName := b.addParamGetName(bcdKey)

return fmt.Sprintf(`wf.ID IN (SELECT ID FROM WebFeatureBrowserCompatFeatures WHERE CompatFeature %s @%s)`,
searchOperatorToSpannerBinaryOperator(op), paramName)
}

// Exclude all that do not have an entry in ExcludedFeatureKeys.
const removeExcludedKeyFilter = "efk.FeatureKey IS NULL"
const removeExcludedKeyFilterAND = "AND " + removeExcludedKeyFilter
Expand Down
25 changes: 25 additions & 0 deletions lib/gcpspanner/feature_search_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ var (
},
}

simpleBCDQuery = TestTree{
Query: `bcd:"html.elements.address"`,
InputTree: &searchtypes.SearchNode{
Keyword: searchtypes.KeywordRoot,
Term: nil,
Children: []*searchtypes.SearchNode{
{
Keyword: searchtypes.KeywordNone,
Term: &searchtypes.SearchTerm{
Identifier: "bcd",
Value: "html.elements.address",
Operator: searchtypes.OperatorEq,
},
},
},
},
}

availableOnBaselineStatus = TestTree{
Query: "available_on:chrome AND baseline_status:widely",
InputTree: &searchtypes.SearchNode{
Expand Down Expand Up @@ -460,6 +478,13 @@ WHERE BrowserName = @param0)`},
"param0": "%" + "grid" + "%",
},
},
{
inputTestTree: simpleBCDQuery,
expectedClauses: []string{`wf.ID IN (SELECT ID FROM WebFeatureBrowserCompatFeatures WHERE CompatFeature = @param0)`},
expectedParams: map[string]interface{}{
"param0": "html.elements.address",
},
},
{
inputTestTree: availableOnBaselineStatus,
expectedClauses: []string{`wf.ID IN (SELECT WebFeatureID FROM BrowserFeatureAvailabilities
Expand Down
9 changes: 9 additions & 0 deletions lib/gcpspanner/searchtypes/features_search_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ func (v *FeaturesSearchVisitor) createNameNode(nameNode antlr.TerminalNode) *Sea
return v.createSimpleNode(nameNode, IdentifierName)
}

func (v *FeaturesSearchVisitor) createBrowserCompatDataNode(bcdNode antlr.TerminalNode) *SearchNode {
return v.createSimpleNode(bcdNode, IdentifierBrowserCompatData)
}

func (v *FeaturesSearchVisitor) createSimpleNode(
node antlr.TerminalNode,
identifier SearchIdentifier) *SearchNode {
Expand Down Expand Up @@ -512,6 +516,11 @@ func (v *FeaturesSearchVisitor) VisitName_term(ctx *parser.Name_termContext) int
return v.createNameNode(ctx.ANY_VALUE())
}

// nolint: revive // Method signature is generated.
func (v *FeaturesSearchVisitor) VisitBcd_term(ctx *parser.Bcd_termContext) interface{} {
return v.createBrowserCompatDataNode(ctx.ANY_VALUE())
}

func (v *FeaturesSearchVisitor) VisitTerm(ctx *parser.TermContext) interface{} {
return v.VisitChildren(ctx)
}
Expand Down
1 change: 1 addition & 0 deletions lib/gcpspanner/searchtypes/searchtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const (
IdentifierAvailableOn SearchIdentifier = "available_on"
IdentifierBaselineDate SearchIdentifier = "baseline_date"
IdentifierBaselineStatus SearchIdentifier = "baseline_status"
IdentifierBrowserCompatData SearchIdentifier = "bcd"
IdentifierName SearchIdentifier = "name"
IdentifierGroup SearchIdentifier = "group"
IdentifierSnapshot SearchIdentifier = "snapshot"
Expand Down
10 changes: 10 additions & 0 deletions lib/gcpspanner/spanneradapters/web_features_consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type WebFeatureSpannerClient interface {
UpsertFeatureDiscouragedDetails(ctx context.Context, featureID string,
in gcpspanner.FeatureDiscouragedDetails) error
PrecalculateBrowserFeatureSupportEvents(ctx context.Context, startAt, endAt time.Time) error
UpsertBrowserCompatFeatures(ctx context.Context, featureID string, compatFeatures []string) error
}

// NewWebFeaturesConsumer constructs an adapter for the web features consumer service.
Expand Down Expand Up @@ -99,6 +100,15 @@ func (c *WebFeaturesConsumer) InsertWebFeatures(
return nil, err
}

if len(featureData.CompatFeatures) > 0 {
err = c.client.UpsertBrowserCompatFeatures(ctx, *id, featureData.CompatFeatures)
if err != nil {
slog.ErrorContext(ctx, "unable to insert compat features", "featureID", *id, "error", err)

return nil, err
}
}

if featureData.Discouraged != nil {
err = c.client.UpsertFeatureDiscouragedDetails(ctx, featureID, gcpspanner.FeatureDiscouragedDetails{
AccordingTo: featureData.Discouraged.AccordingTo,
Expand Down
Loading