Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 1 addition & 1 deletion lib/gcpspanner/baseline_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (m baselineStatusMapper) Merge(in spannerFeatureBaselineStatus,
}
}

func (m baselineStatusMapper) GetKey(in spannerFeatureBaselineStatus) string {
func (m baselineStatusMapper) GetKeyFromExternal(in spannerFeatureBaselineStatus) string {
return in.WebFeatureID
}

Expand Down
3 changes: 2 additions & 1 deletion lib/gcpspanner/browser_availabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func (m browserFeatureAvailabilityMapper) SelectOne(key browserFeatureAvailabili
return stmt
}

func (m browserFeatureAvailabilityMapper) GetKey(in spannerBrowserFeatureAvailability) browserFeatureAvailabilityKey {
func (m browserFeatureAvailabilityMapper) GetKeyFromExternal(
in spannerBrowserFeatureAvailability) browserFeatureAvailabilityKey {
return browserFeatureAvailabilityKey{
WebFeatureID: in.WebFeatureID,
BrowserName: in.BrowserName,
Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/browser_releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (m browserReleaseSpannerMapper) Merge(_ BrowserRelease, existing spannerBro
return existing
}

func (m browserReleaseSpannerMapper) GetKey(in BrowserRelease) browserReleaseKey {
func (m browserReleaseSpannerMapper) GetKeyFromExternal(in BrowserRelease) browserReleaseKey {
return browserReleaseKey{
BrowserName: in.BrowserName,
BrowserVersion: in.BrowserVersion,
Expand Down
3 changes: 2 additions & 1 deletion lib/gcpspanner/chromium_histogram_enum_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ func (m chromiumHistogramEnumValuesMapper) SelectOne(key spannerChromiumHistogra
return stmt
}

func (m chromiumHistogramEnumValuesMapper) GetKey(in ChromiumHistogramEnumValue) spannerChromiumHistogramEnumValueKey {
func (m chromiumHistogramEnumValuesMapper) GetKeyFromExternal(
in ChromiumHistogramEnumValue) spannerChromiumHistogramEnumValueKey {
return spannerChromiumHistogramEnumValueKey{
ChromiumHistogramEnumID: in.ChromiumHistogramEnumID,
BucketID: in.BucketID,
Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/chromium_histogram_enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (m chromiumHistogramEnumsMapper) SelectOne(histogramName string) spanner.St
return stmt
}

func (m chromiumHistogramEnumsMapper) GetKey(in ChromiumHistogramEnum) string {
func (m chromiumHistogramEnumsMapper) GetKeyFromExternal(in ChromiumHistogramEnum) string {
return in.HistogramName
}

Expand Down
365 changes: 277 additions & 88 deletions lib/gcpspanner/client.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type dailyChromiumHistogramEnumCapstoneKey struct {
Day civil.Date
}

func (m dailyChromiumHistogramEnumCapstonesSpannerMapper) GetKey(
func (m dailyChromiumHistogramEnumCapstonesSpannerMapper) GetKeyFromExternal(
in spannerDailyChromiumHistogramEnumCapstone) dailyChromiumHistogramEnumCapstoneKey {
return dailyChromiumHistogramEnumCapstoneKey{
ChromiumHistogramEnumID: in.ChromiumHistogramEnumID,
Expand Down
3 changes: 2 additions & 1 deletion lib/gcpspanner/delete_user_saved_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ type removeUserSavedSearchMapper struct{}

func (m removeUserSavedSearchMapper) Table() string { return savedSearchesTable }

func (m removeUserSavedSearchMapper) GetKey(in DeleteUserSavedSearchRequest) removeUserSavedSearchMapperKey {
func (m removeUserSavedSearchMapper) GetKeyFromExternal(
in DeleteUserSavedSearchRequest) removeUserSavedSearchMapperKey {
return removeUserSavedSearchMapperKey{
ID: in.SavedSearchID,
UserID: in.RequestingUserID,
Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/feature_discouraged_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type spannerFeatureDiscouragedDetails struct {
// Implements the Mapping interface for FeatureDiscouragedDetails and SpannerFeatureDiscouragedDetails.
type featureDiscouragedDetailsSpannerMapper struct{}

func (m featureDiscouragedDetailsSpannerMapper) GetKey(in spannerFeatureDiscouragedDetails) string {
func (m featureDiscouragedDetailsSpannerMapper) GetKeyFromExternal(in spannerFeatureDiscouragedDetails) string {
return in.WebFeatureID
}

Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (m groupSpannerMapper) Merge(in Group, existing spannerGroup) spannerGroup
}
}

func (m groupSpannerMapper) GetKey(in Group) string {
func (m groupSpannerMapper) GetKeyFromExternal(in Group) string {
return in.GroupKey
}

Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/snapshots.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (m snapshotSpannerMapper) Merge(in Snapshot, existing spannerSnapshot) span
}
}

func (m snapshotSpannerMapper) GetKey(in Snapshot) string {
func (m snapshotSpannerMapper) GetKeyFromExternal(in Snapshot) string {
return in.SnapshotKey
}

Expand Down
4 changes: 3 additions & 1 deletion lib/gcpspanner/update_user_saved_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ type updateUserSavedSearchMapper struct {
unauthenticatedUserSavedSearchMapper
}

func (m updateUserSavedSearchMapper) GetKey(in UpdateSavedSearchRequest) string { return in.ID }
func (m updateUserSavedSearchMapper) GetKeyFromExternal(in UpdateSavedSearchRequest) string {
return in.ID
}

func (m updateUserSavedSearchMapper) Table() string { return savedSearchesTable }

Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/user_search_bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type userSavedSearchBookmarkKey struct {
UserSavedSearchBookmark
}

func (m userSavedSearchBookmarkMapper) GetKey(
func (m userSavedSearchBookmarkMapper) GetKeyFromExternal(
in UserSavedSearchBookmark) userSavedSearchBookmarkKey {
return userSavedSearchBookmarkKey{
UserSavedSearchBookmark: in,
Expand Down
3 changes: 2 additions & 1 deletion lib/gcpspanner/web_feature_chromium_histograms.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ type spannerWebFeatureChromiumHistogramEnum struct {
// Implements the Mapping interface for WebFeatureChromiumHistogramEnum and SpannerWebFeatureChromiumHistogramEnum.
type webFeaturesChromiumHistogramEnumSpannerMapper struct{}

func (m webFeaturesChromiumHistogramEnumSpannerMapper) GetKey(in WebFeatureChromiumHistogramEnumValue) string {
func (m webFeaturesChromiumHistogramEnumSpannerMapper) GetKeyFromExternal(
in WebFeatureChromiumHistogramEnumValue) string {
return in.WebFeatureID
}

Expand Down
2 changes: 1 addition & 1 deletion lib/gcpspanner/web_feature_snapshots.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type spannerWebFeatureSnapshot struct {
// Implements the Mapping interface for WebFeatureSnapshot and SpannerWebFeatureSnapshot.
type webFeaturesSnapshotSpannerMapper struct{}

func (m webFeaturesSnapshotSpannerMapper) GetKey(in WebFeatureSnapshot) string {
func (m webFeaturesSnapshotSpannerMapper) GetKeyFromExternal(in WebFeatureSnapshot) string {
return in.WebFeatureID
}

Expand Down
36 changes: 34 additions & 2 deletions lib/gcpspanner/web_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"cmp"
"context"
"fmt"
"log/slog"

"cloud.google.com/go/spanner"
)
Expand All @@ -42,9 +43,17 @@ type WebFeature struct {
DescriptionHTML string `spanner:"DescriptionHtml"`
}

// Implements the entityMapper interface for WebFeature and SpannerWebFeature.
// Implements the syncableEntityMapper interface for WebFeature and SpannerWebFeature.
type webFeatureSpannerMapper struct{}

// SelectAll returns a statement to select all WebFeatures.
func (m webFeatureSpannerMapper) SelectAll() spanner.Statement {
return spanner.NewStatement(fmt.Sprintf(`
SELECT
ID, FeatureKey, Name, Description, DescriptionHtml
FROM %s`, m.Table()))
}

func (m webFeatureSpannerMapper) SelectOne(key string) spanner.Statement {
stmt := spanner.NewStatement(fmt.Sprintf(`
SELECT
Expand Down Expand Up @@ -73,11 +82,24 @@ func (m webFeatureSpannerMapper) Merge(in WebFeature, existing SpannerWebFeature
}
}

// DeleteMutation creates a Spanner delete mutation for a given WebFeature.
// It uses the internal Spanner ID, not the FeatureKey, for the deletion.
func (m webFeatureSpannerMapper) DeleteMutation(in SpannerWebFeature) *spanner.Mutation {
return spanner.Delete(webFeaturesTable, spanner.Key{in.ID})
}

// Table returns the name of the Spanner table.
func (m webFeatureSpannerMapper) Table() string {
return webFeaturesTable
}

func (m webFeatureSpannerMapper) GetKey(in WebFeature) string {
// GetKeyFromExternal returns the business key (FeatureKey) from an external WebFeature struct.
func (m webFeatureSpannerMapper) GetKeyFromExternal(in WebFeature) string {
return in.FeatureKey
}

// GetKeyFromInternal returns the business key (FeatureKey) from an internal SpannerWebFeature struct.
func (m webFeatureSpannerMapper) GetKeyFromInternal(in SpannerWebFeature) string {
return in.FeatureKey
}

Expand All @@ -96,6 +118,16 @@ func (m webFeatureSpannerMapper) GetID(key string) spanner.Statement {
return stmt
}

// SyncWebFeatures reconciles the WebFeatures table with the provided list of features.
// It will insert new features, update existing ones, and delete any features
// that are in the database but not in the provided list.
func (c *Client) SyncWebFeatures(ctx context.Context, features []WebFeature) error {
slog.InfoContext(ctx, "Starting web features synchronization")
synchronizer := newEntitySynchronizer[webFeatureSpannerMapper](c)

return synchronizer.Sync(ctx, features)
}

func (c *Client) UpsertWebFeature(ctx context.Context, feature WebFeature) (*string, error) {
return newEntityWriterWithIDRetrieval[webFeatureSpannerMapper, string](c).upsertAndGetID(ctx, feature)
}
Expand Down
115 changes: 115 additions & 0 deletions lib/gcpspanner/web_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

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

Expand Down Expand Up @@ -176,3 +177,117 @@ func TestUpsertWebFeature(t *testing.T) {
t.Errorf("unequal keys. expected %+v actual %+v", expectedKeys, keys)
}
}

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

type syncTestCase struct {
name string
initialState []WebFeature
desiredState []WebFeature
expectedState []WebFeature
}

testCases := []syncTestCase{
{
name: "Initial creation",
initialState: nil, // No initial state
desiredState: getSampleFeatures(),
expectedState: getSampleFeatures(),
},
{
name: "Deletes features not in desired state",
initialState: getSampleFeatures(),
desiredState: []WebFeature{
getSampleFeatures()[0], // feature1
getSampleFeatures()[2], // feature3
},
expectedState: []WebFeature{
getSampleFeatures()[0],
getSampleFeatures()[2],
},
},
{
name: "Updates existing features",
initialState: getSampleFeatures(),
desiredState: func() []WebFeature {
features := getSampleFeatures()
features[1].Name = "UPDATED Feature 2"
features[3].Description = "UPDATED Description 4"

return features
}(),
expectedState: func() []WebFeature {
features := getSampleFeatures()
features[1].Name = "UPDATED Feature 2"
features[3].Description = "UPDATED Description 4"

return features
}(),
},
{
name: "Performs mixed insert, update, and delete",
initialState: getSampleFeatures(),
desiredState: []WebFeature{
{FeatureKey: "feature1", Name: "Updated Feature 1 Name", Description: "", DescriptionHTML: ""},
getSampleFeatures()[2], // Keep feature3
{FeatureKey: "feature5", Name: "New Feature 5", Description: "", DescriptionHTML: ""},
},
expectedState: []WebFeature{
{
FeatureKey: "feature1",
Name: "Updated Feature 1 Name",
Description: "Wow what a feature description", // Preserved by merge logic
DescriptionHTML: "Feature <b>1</b> description", // Preserved by merge logic
},
getSampleFeatures()[2], // feature3 is unchanged
{
FeatureKey: "feature5",
Name: "New Feature 5",
Description: "", // New fields are empty
DescriptionHTML: "",
},
},
},
{
name: "No changes when desired state matches current state",
initialState: getSampleFeatures(),
desiredState: getSampleFeatures(),
expectedState: getSampleFeatures(),
},
{
name: "Deletes all features when desired state is empty",
initialState: getSampleFeatures(),
desiredState: []WebFeature{},
expectedState: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
restartDatabaseContainer(t)

// 1. Setup initial state if provided
if tc.initialState != nil {
if err := spannerClient.SyncWebFeatures(ctx, tc.initialState); err != nil {
t.Fatalf("Failed to set up initial state: %v", err)
}
}

// 2. Run the sync with the desired state
if err := spannerClient.SyncWebFeatures(ctx, tc.desiredState); err != nil {
t.Fatalf("SyncWebFeatures failed: %v", err)
}

// 3. Verify the final state
featuresInDB, err := spannerClient.ReadAllWebFeatures(ctx, t)
if err != nil {
t.Fatalf("ReadAllWebFeatures failed: %v", err)
}

if diff := cmp.Diff(tc.expectedState, featuresInDB); diff != "" {
t.Errorf("features mismatch (-want +got):\n%s", diff)
}
})
}
}
2 changes: 1 addition & 1 deletion lib/gcpspanner/wpt_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (m wptRunSpannerMapper) SelectOne(externalRunID int64) spanner.Statement {
return stmt
}

func (m wptRunSpannerMapper) GetKey(in WPTRun) int64 {
func (m wptRunSpannerMapper) GetKeyFromExternal(in WPTRun) int64 {
return in.RunID
}

Expand Down
1 change: 1 addition & 0 deletions lib/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/go-cmp v0.7.0
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0
Expand Down