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 }