go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/bq/schemaapplyer_test.go (about)

     1  // Copyright 2021 The LUCI Authors.
     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 bq
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"testing"
    21  	"time"
    22  
    23  	"cloud.google.com/go/bigquery"
    24  	"google.golang.org/api/googleapi"
    25  
    26  	. "github.com/smartystreets/goconvey/convey"
    27  	"go.chromium.org/luci/common/clock/testclock"
    28  	. "go.chromium.org/luci/common/testing/assertions"
    29  	"go.chromium.org/luci/server/caching"
    30  )
    31  
    32  type tableMock struct {
    33  	fullyQualifiedName string
    34  
    35  	md      *bigquery.TableMetadata
    36  	mdCalls int
    37  	mdErr   error
    38  
    39  	createMD  *bigquery.TableMetadata
    40  	createErr error
    41  
    42  	updateMD  *bigquery.TableMetadataToUpdate
    43  	updateErr error
    44  }
    45  
    46  func (t *tableMock) FullyQualifiedName() string {
    47  	return t.fullyQualifiedName
    48  }
    49  
    50  func (t *tableMock) Metadata(ctx context.Context, opts ...bigquery.TableMetadataOption) (*bigquery.TableMetadata, error) {
    51  	t.mdCalls++
    52  	return t.md, t.mdErr
    53  }
    54  
    55  func (t *tableMock) Create(ctx context.Context, md *bigquery.TableMetadata) error {
    56  	t.createMD = md
    57  	return t.createErr
    58  }
    59  
    60  func (t *tableMock) Update(ctx context.Context, md bigquery.TableMetadataToUpdate, etag string, opts ...bigquery.TableUpdateOption) (*bigquery.TableMetadata, error) {
    61  	t.updateMD = &md
    62  	return t.md, t.updateErr
    63  }
    64  
    65  var cache = RegisterSchemaApplyerCache(50)
    66  
    67  func TestBqTableCache(t *testing.T) {
    68  	t.Parallel()
    69  	Convey(`TestCheckBqTableCache`, t, func() {
    70  		ctx := context.Background()
    71  		referenceTime := time.Date(2030, time.February, 3, 4, 5, 6, 7, time.UTC)
    72  		ctx, tc := testclock.UseTime(ctx, referenceTime)
    73  		ctx = caching.WithEmptyProcessCache(ctx)
    74  
    75  		t := &tableMock{
    76  			fullyQualifiedName: "project.dataset.table",
    77  			md:                 &bigquery.TableMetadata{},
    78  		}
    79  
    80  		sa := NewSchemaApplyer(cache)
    81  		rowSchema := bigquery.Schema{
    82  			{
    83  				Name:   "exported",
    84  				Type:   bigquery.RecordFieldType,
    85  				Schema: bigquery.Schema{{Name: "id"}},
    86  			},
    87  			{
    88  				Name:   "tags",
    89  				Type:   bigquery.RecordFieldType,
    90  				Schema: bigquery.Schema{{Name: "key"}, {Name: "value"}},
    91  			},
    92  			{
    93  				Name: "created_time",
    94  				Type: bigquery.TimestampFieldType,
    95  			},
    96  		}
    97  		table := &bigquery.TableMetadata{
    98  			Schema: rowSchema,
    99  		}
   100  
   101  		Convey(`Table does not exist`, func() {
   102  			t.mdErr = &googleapi.Error{Code: http.StatusNotFound}
   103  			err := sa.EnsureTable(ctx, t, table)
   104  			So(err, ShouldBeNil)
   105  			So(t.createMD.Schema, ShouldResemble, rowSchema)
   106  		})
   107  
   108  		Convey(`Table is missing fields`, func() {
   109  			t.md.Schema = bigquery.Schema{
   110  				{
   111  					Name: "legacy",
   112  				},
   113  				{
   114  					Name:   "exported",
   115  					Schema: bigquery.Schema{{Name: "legacy"}},
   116  				},
   117  			}
   118  			err := sa.EnsureTable(ctx, t, table)
   119  			So(err, ShouldBeNil)
   120  
   121  			So(t.updateMD, ShouldNotBeNil) // The table was updated.
   122  			So(len(t.updateMD.Schema), ShouldBeGreaterThan, 3)
   123  			So(t.updateMD.Schema[0].Name, ShouldEqual, "legacy")
   124  			So(t.updateMD.Schema[1].Name, ShouldEqual, "exported")
   125  			So(t.updateMD.Schema[1].Schema[0].Name, ShouldEqual, "legacy")
   126  			So(t.updateMD.Schema[1].Schema[1].Name, ShouldEqual, "id") // new field
   127  			So(t.updateMD.Schema[1].Schema[1].Required, ShouldBeFalse) // relaxed
   128  		})
   129  
   130  		Convey(`Table is up to date`, func() {
   131  			t.md.Schema = rowSchema
   132  			err := sa.EnsureTable(ctx, t, table)
   133  			So(err, ShouldBeNil)
   134  			So(t.updateMD, ShouldBeNil) // we did not try to update it
   135  		})
   136  
   137  		Convey(`Invalid attempt to convert regular table into view`, func() {
   138  			table.ViewQuery = "SELECT * FROM a"
   139  			err := sa.EnsureTable(ctx, t, table)
   140  			So(err, ShouldErrLike, "cannot change a regular table into a view table")
   141  			So(t.updateMD, ShouldBeNil)
   142  		})
   143  
   144  		Convey(`Views`, func() {
   145  			mockTable := &tableMock{
   146  				fullyQualifiedName: "project.dataset.table",
   147  				md: &bigquery.TableMetadata{
   148  					Type:      bigquery.ViewTable,
   149  					ViewQuery: "SELECT * FROM a",
   150  				},
   151  			}
   152  			spec := &bigquery.TableMetadata{ViewQuery: "SELECT * FROM a"}
   153  
   154  			Convey("With UpdateMetadata option", func() {
   155  				mockTable.md.Labels = map[string]string{
   156  					MetadataVersionKey: "9",
   157  				}
   158  				spec.Labels = map[string]string{
   159  					MetadataVersionKey: "9",
   160  				}
   161  
   162  				Convey(`View is up to date`, func() {
   163  					err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   164  					So(err, ShouldBeNil)
   165  					So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   166  				})
   167  
   168  				Convey(`View requires update`, func() {
   169  					spec.ViewQuery = "SELECT * FROM b"
   170  					spec.Labels[MetadataVersionKey] = "10"
   171  					err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   172  					So(err, ShouldBeNil)
   173  
   174  					expectedUpdate := &bigquery.TableMetadataToUpdate{
   175  						ViewQuery: "SELECT * FROM b",
   176  					}
   177  					expectedUpdate.SetLabel(MetadataVersionKey, "10")
   178  					So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   179  				})
   180  
   181  				Convey(`View different but no new metadata version`, func() {
   182  					spec.ViewQuery = "SELECT * FROM b"
   183  					err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   184  					So(err, ShouldBeNil)
   185  					So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   186  				})
   187  
   188  				Convey(`View requires update but not enforced`, func() {
   189  					spec.ViewQuery = "SELECT * FROM b"
   190  					spec.Labels[MetadataVersionKey] = "10"
   191  					err := EnsureTable(ctx, mockTable, spec)
   192  					So(err, ShouldBeNil)
   193  					So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   194  				})
   195  
   196  				Convey(`View is up to date, new metadata version and RefreshViewInterval enabled`, func() {
   197  					mockTable.md.ViewQuery = "-- Indirect schema version: 2030-02-03T03:55:50Z\nSELECT * FROM a"
   198  					spec.Labels[MetadataVersionKey] = "10"
   199  
   200  					err := EnsureTable(ctx, mockTable, spec, UpdateMetadata(), RefreshViewInterval(1*time.Hour))
   201  					So(err, ShouldBeNil)
   202  
   203  					// No update is applied except for the new metadata version.
   204  					expectedUpdate := &bigquery.TableMetadataToUpdate{}
   205  					expectedUpdate.SetLabel(MetadataVersionKey, "10")
   206  					So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   207  				})
   208  
   209  				Convey(`View requires update and RefreshViewInterval enabled`, func() {
   210  					mockTable.md.ViewQuery = "-- Indirect schema version: 2030-02-03T03:55:50Z\nSELECT * FROM a"
   211  					spec.ViewQuery = "SELECT * FROM b"
   212  					spec.Labels[MetadataVersionKey] = "10"
   213  
   214  					err := EnsureTable(ctx, mockTable, spec, UpdateMetadata(), RefreshViewInterval(1*time.Hour))
   215  					So(err, ShouldBeNil)
   216  
   217  					expectedUpdate := &bigquery.TableMetadataToUpdate{
   218  						ViewQuery: "-- Indirect schema version: 2030-02-03T04:05:06Z\nSELECT * FROM b",
   219  					}
   220  					expectedUpdate.SetLabel(MetadataVersionKey, "10")
   221  					So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   222  				})
   223  
   224  			})
   225  			Convey(`With RefreshViewInterval option`, func() {
   226  				spec.ViewQuery = "should be ignored as this option does not push spec.ViewQuery"
   227  
   228  				Convey(`View is not stale`, func() {
   229  					mockTable.md.ViewQuery = "-- Indirect schema version: 2030-02-03T03:55:50Z\nSELECT * FROM a"
   230  
   231  					err := EnsureTable(ctx, mockTable, spec, RefreshViewInterval(1*time.Hour))
   232  					So(err, ShouldBeNil)
   233  
   234  					So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   235  				})
   236  				Convey(`View is stale`, func() {
   237  					mockTable.md.ViewQuery = "-- Indirect schema version: 2030-02-03T03:04:00Z\nSELECT * FROM a"
   238  
   239  					err := EnsureTable(ctx, mockTable, spec, RefreshViewInterval(1*time.Hour))
   240  					So(err, ShouldBeNil)
   241  
   242  					expectedUpdate := &bigquery.TableMetadataToUpdate{
   243  						ViewQuery: "-- Indirect schema version: 2030-02-03T04:05:06Z\nSELECT * FROM a",
   244  					}
   245  					So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   246  				})
   247  				Convey(`View has no header`, func() {
   248  					mockTable.md.ViewQuery = "SELECT * FROM a"
   249  
   250  					err := EnsureTable(ctx, mockTable, spec, RefreshViewInterval(1*time.Hour))
   251  					So(err, ShouldBeNil)
   252  
   253  					expectedUpdate := &bigquery.TableMetadataToUpdate{
   254  						ViewQuery: "-- Indirect schema version: 2030-02-03T04:05:06Z\nSELECT * FROM a",
   255  					}
   256  					So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   257  				})
   258  			})
   259  		})
   260  
   261  		Convey(`Description`, func() {
   262  			mockTable := &tableMock{
   263  				fullyQualifiedName: "project.dataset.table",
   264  				md: &bigquery.TableMetadata{
   265  					Type:        bigquery.ViewTable,
   266  					ViewQuery:   "SELECT * FROM a",
   267  					Description: "Description A",
   268  					Labels: map[string]string{
   269  						MetadataVersionKey: "1",
   270  					},
   271  				},
   272  			}
   273  			spec := &bigquery.TableMetadata{
   274  				ViewQuery:   "SELECT * FROM a",
   275  				Description: "Description A",
   276  				Labels: map[string]string{
   277  					MetadataVersionKey: "1",
   278  				},
   279  			}
   280  
   281  			Convey(`Description is up to date`, func() {
   282  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   283  				So(err, ShouldBeNil)
   284  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   285  			})
   286  
   287  			Convey(`Description requires update`, func() {
   288  				spec.Description = "Description B"
   289  				spec.Labels[MetadataVersionKey] = "2"
   290  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   291  				So(err, ShouldBeNil)
   292  
   293  				expectedUpdate := &bigquery.TableMetadataToUpdate{
   294  					Description: "Description B",
   295  				}
   296  				expectedUpdate.SetLabel(MetadataVersionKey, "2")
   297  				So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   298  			})
   299  
   300  			Convey(`Description different but no new metadata version`, func() {
   301  				spec.Description = "Description B"
   302  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   303  				So(err, ShouldBeNil)
   304  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   305  			})
   306  
   307  			Convey(`Description requires update but not enforced`, func() {
   308  				spec.Description = "Description B"
   309  				spec.Labels[MetadataVersionKey] = "2"
   310  				err := EnsureTable(ctx, mockTable, spec)
   311  				So(err, ShouldBeNil)
   312  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   313  			})
   314  		})
   315  
   316  		Convey(`Labels`, func() {
   317  			mockTable := &tableMock{
   318  				fullyQualifiedName: "project.dataset.table",
   319  				md: &bigquery.TableMetadata{
   320  					Type:      bigquery.ViewTable,
   321  					ViewQuery: "SELECT * FROM a",
   322  					Labels:    map[string]string{MetadataVersionKey: "1", "key-a": "value-a", "key-b": "value-b", "key-c": "value-c"},
   323  				},
   324  			}
   325  			spec := &bigquery.TableMetadata{
   326  				ViewQuery: "SELECT * FROM a",
   327  				Labels:    map[string]string{MetadataVersionKey: "1", "key-a": "value-a", "key-b": "value-b", "key-c": "value-c"},
   328  			}
   329  
   330  			Convey(`Labels are up to date`, func() {
   331  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   332  				So(err, ShouldBeNil)
   333  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   334  			})
   335  
   336  			Convey(`Labels require update`, func() {
   337  				spec.Labels = map[string]string{MetadataVersionKey: "2", "key-a": "value-a", "key-b": "new-value-b", "key-d": "value-d"}
   338  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   339  				So(err, ShouldBeNil)
   340  
   341  				update := &bigquery.TableMetadataToUpdate{}
   342  				update.DeleteLabel("key-c")
   343  				update.SetLabel(MetadataVersionKey, "2")
   344  				update.SetLabel("key-b", "new-value-b")
   345  				update.SetLabel("key-d", "value-d")
   346  				So(mockTable.updateMD, ShouldResemble, update)
   347  			})
   348  
   349  			Convey(`Labels require update but no new metadata version`, func() {
   350  				spec.Labels = map[string]string{MetadataVersionKey: "1", "key-a": "value-a", "key-b": "new-value-b", "key-d": "value-d"}
   351  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   352  				So(err, ShouldBeNil)
   353  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   354  			})
   355  
   356  			Convey(`Labels require update but not enforced`, func() {
   357  				spec.Labels = map[string]string{MetadataVersionKey: "2", "key-a": "value-a", "key-b": "new-value-b", "key-d": "value-d"}
   358  				err := EnsureTable(ctx, mockTable, spec)
   359  				So(err, ShouldBeNil)
   360  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   361  			})
   362  		})
   363  
   364  		Convey(`Clustering`, func() {
   365  			mockTable := &tableMock{
   366  				fullyQualifiedName: "project.dataset.table",
   367  				md: &bigquery.TableMetadata{
   368  					Clustering: &bigquery.Clustering{Fields: []string{"field_a", "field_b"}},
   369  					Labels: map[string]string{
   370  						MetadataVersionKey: "1",
   371  					},
   372  				},
   373  			}
   374  			spec := &bigquery.TableMetadata{
   375  				Clustering: &bigquery.Clustering{Fields: []string{"field_a", "field_b"}},
   376  				Labels: map[string]string{
   377  					MetadataVersionKey: "1",
   378  				},
   379  			}
   380  
   381  			Convey(`Clustering is up to date`, func() {
   382  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   383  				So(err, ShouldBeNil)
   384  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   385  			})
   386  
   387  			Convey(`Clustering requires update`, func() {
   388  				spec.Clustering = &bigquery.Clustering{Fields: []string{"field_c"}}
   389  				spec.Labels[MetadataVersionKey] = "2"
   390  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   391  				So(err, ShouldBeNil)
   392  
   393  				expectedUpdate := &bigquery.TableMetadataToUpdate{
   394  					Clustering: &bigquery.Clustering{Fields: []string{"field_c"}},
   395  				}
   396  				expectedUpdate.SetLabel(MetadataVersionKey, "2")
   397  				So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   398  			})
   399  
   400  			Convey(`Clustering up to date but new metadata version`, func() {
   401  				spec.Labels[MetadataVersionKey] = "2"
   402  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   403  				So(err, ShouldBeNil)
   404  
   405  				expectedUpdate := &bigquery.TableMetadataToUpdate{}
   406  				expectedUpdate.SetLabel(MetadataVersionKey, "2")
   407  				So(mockTable.updateMD, ShouldResemble, expectedUpdate)
   408  			})
   409  
   410  			Convey(`Clustering different but no new metadata version`, func() {
   411  				spec.Clustering = &bigquery.Clustering{Fields: []string{"field_c"}}
   412  				err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   413  				So(err, ShouldBeNil)
   414  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   415  			})
   416  
   417  			Convey(`Clustering requires update but not enforced`, func() {
   418  				spec.Clustering = &bigquery.Clustering{Fields: []string{"field_c"}}
   419  				spec.Labels[MetadataVersionKey] = "2"
   420  				err := EnsureTable(ctx, mockTable, spec)
   421  				So(err, ShouldBeNil)
   422  				So(mockTable.updateMD, ShouldBeNil) // we did not try to update it
   423  			})
   424  		})
   425  
   426  		Convey(`RefreshViewInterval is used on a table that is not a view`, func() {
   427  			mockTable := &tableMock{
   428  				fullyQualifiedName: "project.dataset.table",
   429  				md:                 nil,
   430  			}
   431  			spec := &bigquery.TableMetadata{}
   432  			err := EnsureTable(ctx, mockTable, spec, RefreshViewInterval(time.Hour))
   433  			So(err, ShouldEqual, errViewRefreshEnabledOnNonView)
   434  		})
   435  
   436  		Convey(`UpdateMetadata is used without spec having a metadata version`, func() {
   437  			mockTable := &tableMock{
   438  				fullyQualifiedName: "project.dataset.table",
   439  				md:                 nil,
   440  			}
   441  			spec := &bigquery.TableMetadata{}
   442  			err := EnsureTable(ctx, mockTable, spec, UpdateMetadata())
   443  			So(err, ShouldEqual, errMetadataVersionLabelMissing)
   444  		})
   445  
   446  		Convey(`Cache is working`, func() {
   447  			err := sa.EnsureTable(ctx, t, table)
   448  			So(err, ShouldBeNil)
   449  			calls := t.mdCalls
   450  
   451  			// Confirms the cache is working.
   452  			err = sa.EnsureTable(ctx, t, table)
   453  			So(err, ShouldBeNil)
   454  			So(t.mdCalls, ShouldEqual, calls) // no more new calls were made.
   455  
   456  			// Confirms the cache is expired as expected.
   457  			tc.Add(6 * time.Minute)
   458  			err = sa.EnsureTable(ctx, t, table)
   459  			So(err, ShouldBeNil)
   460  			So(t.mdCalls, ShouldBeGreaterThan, calls) // new calls were made.
   461  		})
   462  	})
   463  }