1// Copyright 2015 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 15package bigquery 16 17import ( 18 "context" 19 "errors" 20 "fmt" 21 "time" 22 23 "cloud.google.com/go/internal/optional" 24 "cloud.google.com/go/internal/trace" 25 bq "google.golang.org/api/bigquery/v2" 26) 27 28// A Table is a reference to a BigQuery table. 29type Table struct { 30 // ProjectID, DatasetID and TableID may be omitted if the Table is the destination for a query. 31 // In this case the result will be stored in an ephemeral table. 32 ProjectID string 33 DatasetID string 34 // TableID must contain only letters (a-z, A-Z), numbers (0-9), or underscores (_). 35 // The maximum length is 1,024 characters. 36 TableID string 37 38 c *Client 39} 40 41// TableMetadata contains information about a BigQuery table. 42type TableMetadata struct { 43 // The following fields can be set when creating a table. 44 45 // The user-friendly name for the table. 46 Name string 47 48 // Output-only location of the table, based on the encapsulating dataset. 49 Location string 50 51 // The user-friendly description of the table. 52 Description string 53 54 // The table schema. If provided on create, ViewQuery must be empty. 55 Schema Schema 56 57 // If non-nil, this table is a materialized view. 58 MaterializedView *MaterializedViewDefinition 59 60 // The query to use for a logical view. If provided on create, Schema must be nil. 61 ViewQuery string 62 63 // Use Legacy SQL for the view query. 64 // At most one of UseLegacySQL and UseStandardSQL can be true. 65 UseLegacySQL bool 66 67 // Use Standard SQL for the view query. The default. 68 // At most one of UseLegacySQL and UseStandardSQL can be true. 69 // Deprecated: use UseLegacySQL. 70 UseStandardSQL bool 71 72 // If non-nil, the table is partitioned by time. Only one of 73 // time partitioning or range partitioning can be specified. 74 TimePartitioning *TimePartitioning 75 76 // If non-nil, the table is partitioned by integer range. Only one of 77 // time partitioning or range partitioning can be specified. 78 RangePartitioning *RangePartitioning 79 80 // If set to true, queries that reference this table must specify a 81 // partition filter (e.g. a WHERE clause) that can be used to eliminate 82 // partitions. Used to prevent unintentional full data scans on large 83 // partitioned tables. 84 RequirePartitionFilter bool 85 86 // Clustering specifies the data clustering configuration for the table. 87 Clustering *Clustering 88 89 // The time when this table expires. If set, this table will expire at the 90 // specified time. Expired tables will be deleted and their storage 91 // reclaimed. The zero value is ignored. 92 ExpirationTime time.Time 93 94 // User-provided labels. 95 Labels map[string]string 96 97 // Information about a table stored outside of BigQuery. 98 ExternalDataConfig *ExternalDataConfig 99 100 // Custom encryption configuration (e.g., Cloud KMS keys). 101 EncryptionConfig *EncryptionConfig 102 103 // All the fields below are read-only. 104 105 FullID string // An opaque ID uniquely identifying the table. 106 Type TableType 107 CreationTime time.Time 108 LastModifiedTime time.Time 109 110 // The size of the table in bytes. 111 // This does not include data that is being buffered during a streaming insert. 112 NumBytes int64 113 114 // The number of bytes in the table considered "long-term storage" for reduced 115 // billing purposes. See https://cloud.google.com/bigquery/pricing#long-term-storage 116 // for more information. 117 NumLongTermBytes int64 118 119 // The number of rows of data in this table. 120 // This does not include data that is being buffered during a streaming insert. 121 NumRows uint64 122 123 // SnapshotDefinition contains additional information about the provenance of a 124 // given snapshot table. 125 SnapshotDefinition *SnapshotDefinition 126 127 // Contains information regarding this table's streaming buffer, if one is 128 // present. This field will be nil if the table is not being streamed to or if 129 // there is no data in the streaming buffer. 130 StreamingBuffer *StreamingBuffer 131 132 // ETag is the ETag obtained when reading metadata. Pass it to Table.Update to 133 // ensure that the metadata hasn't changed since it was read. 134 ETag string 135} 136 137// TableCreateDisposition specifies the circumstances under which destination table will be created. 138// Default is CreateIfNeeded. 139type TableCreateDisposition string 140 141const ( 142 // CreateIfNeeded will create the table if it does not already exist. 143 // Tables are created atomically on successful completion of a job. 144 CreateIfNeeded TableCreateDisposition = "CREATE_IF_NEEDED" 145 146 // CreateNever ensures the table must already exist and will not be 147 // automatically created. 148 CreateNever TableCreateDisposition = "CREATE_NEVER" 149) 150 151// TableWriteDisposition specifies how existing data in a destination table is treated. 152// Default is WriteAppend. 153type TableWriteDisposition string 154 155const ( 156 // WriteAppend will append to any existing data in the destination table. 157 // Data is appended atomically on successful completion of a job. 158 WriteAppend TableWriteDisposition = "WRITE_APPEND" 159 160 // WriteTruncate overrides the existing data in the destination table. 161 // Data is overwritten atomically on successful completion of a job. 162 WriteTruncate TableWriteDisposition = "WRITE_TRUNCATE" 163 164 // WriteEmpty fails writes if the destination table already contains data. 165 WriteEmpty TableWriteDisposition = "WRITE_EMPTY" 166) 167 168// TableType is the type of table. 169type TableType string 170 171const ( 172 // RegularTable is a regular table. 173 RegularTable TableType = "TABLE" 174 // ViewTable is a table type describing that the table is a logical view. 175 // See more information at https://cloud.google.com/bigquery/docs/views. 176 ViewTable TableType = "VIEW" 177 // ExternalTable is a table type describing that the table is an external 178 // table (also known as a federated data source). See more information at 179 // https://cloud.google.com/bigquery/external-data-sources. 180 ExternalTable TableType = "EXTERNAL" 181 // MaterializedView represents a managed storage table that's derived from 182 // a base table. 183 MaterializedView TableType = "MATERIALIZED_VIEW" 184 // Snapshot represents an immutable point in time snapshot of some other 185 // table. 186 Snapshot TableType = "SNAPSHOT" 187) 188 189// MaterializedViewDefinition contains information for materialized views. 190type MaterializedViewDefinition struct { 191 // EnableRefresh governs whether the derived view is updated to reflect 192 // changes in the base table. 193 EnableRefresh bool 194 195 // LastRefreshTime reports the time, in millisecond precision, that the 196 // materialized view was last updated. 197 LastRefreshTime time.Time 198 199 // Query contains the SQL query used to define the materialized view. 200 Query string 201 202 // RefreshInterval defines the maximum frequency, in millisecond precision, 203 // at which this this materialized view will be refreshed. 204 RefreshInterval time.Duration 205} 206 207func (mvd *MaterializedViewDefinition) toBQ() *bq.MaterializedViewDefinition { 208 if mvd == nil { 209 return nil 210 } 211 return &bq.MaterializedViewDefinition{ 212 EnableRefresh: mvd.EnableRefresh, 213 Query: mvd.Query, 214 LastRefreshTime: mvd.LastRefreshTime.UnixNano() / 1e6, 215 RefreshIntervalMs: int64(mvd.RefreshInterval) / 1e6, 216 // force sending the bool in all cases due to how Go handles false. 217 ForceSendFields: []string{"EnableRefresh"}, 218 } 219} 220 221func bqToMaterializedViewDefinition(q *bq.MaterializedViewDefinition) *MaterializedViewDefinition { 222 if q == nil { 223 return nil 224 } 225 return &MaterializedViewDefinition{ 226 EnableRefresh: q.EnableRefresh, 227 Query: q.Query, 228 LastRefreshTime: unixMillisToTime(q.LastRefreshTime), 229 RefreshInterval: time.Duration(q.RefreshIntervalMs) * time.Millisecond, 230 } 231} 232 233// SnapshotDefinition provides metadata related to the origin of a snapshot. 234type SnapshotDefinition struct { 235 236 // BaseTableReference describes the ID of the table that this snapshot 237 // came from. 238 BaseTableReference *Table 239 240 // SnapshotTime indicates when the base table was snapshot. 241 SnapshotTime time.Time 242} 243 244func (sd *SnapshotDefinition) toBQ() *bq.SnapshotDefinition { 245 if sd == nil { 246 return nil 247 } 248 return &bq.SnapshotDefinition{ 249 BaseTableReference: sd.BaseTableReference.toBQ(), 250 SnapshotTime: sd.SnapshotTime.Format(time.RFC3339), 251 } 252} 253 254func bqToSnapshotDefinition(q *bq.SnapshotDefinition, c *Client) *SnapshotDefinition { 255 if q == nil { 256 return nil 257 } 258 sd := &SnapshotDefinition{ 259 BaseTableReference: bqToTable(q.BaseTableReference, c), 260 } 261 // It's possible we could fail to populate SnapshotTime if we fail to parse 262 // the backend representation. 263 if t, err := time.Parse(time.RFC3339, q.SnapshotTime); err == nil { 264 sd.SnapshotTime = t 265 } 266 return sd 267} 268 269// TimePartitioningType defines the interval used to partition managed data. 270type TimePartitioningType string 271 272const ( 273 // DayPartitioningType uses a day-based interval for time partitioning. 274 DayPartitioningType TimePartitioningType = "DAY" 275 276 // HourPartitioningType uses an hour-based interval for time partitioning. 277 HourPartitioningType TimePartitioningType = "HOUR" 278 279 // MonthPartitioningType uses a month-based interval for time partitioning. 280 MonthPartitioningType TimePartitioningType = "MONTH" 281 282 // YearPartitioningType uses a year-based interval for time partitioning. 283 YearPartitioningType TimePartitioningType = "YEAR" 284) 285 286// TimePartitioning describes the time-based date partitioning on a table. 287// For more information see: https://cloud.google.com/bigquery/docs/creating-partitioned-tables. 288type TimePartitioning struct { 289 // Defines the partition interval type. Supported values are "HOUR", "DAY", "MONTH", and "YEAR". 290 // When the interval type is not specified, default behavior is DAY. 291 Type TimePartitioningType 292 293 // The amount of time to keep the storage for a partition. 294 // If the duration is empty (0), the data in the partitions do not expire. 295 Expiration time.Duration 296 297 // If empty, the table is partitioned by pseudo column '_PARTITIONTIME'; if set, the 298 // table is partitioned by this field. The field must be a top-level TIMESTAMP or 299 // DATE field. Its mode must be NULLABLE or REQUIRED. 300 Field string 301 302 // If set to true, queries that reference this table must specify a 303 // partition filter (e.g. a WHERE clause) that can be used to eliminate 304 // partitions. Used to prevent unintentional full data scans on large 305 // partitioned tables. 306 // DEPRECATED: use the top-level RequirePartitionFilter in TableMetadata. 307 RequirePartitionFilter bool 308} 309 310func (p *TimePartitioning) toBQ() *bq.TimePartitioning { 311 if p == nil { 312 return nil 313 } 314 // Treat unspecified values as DAY-based partitioning. 315 intervalType := DayPartitioningType 316 if p.Type != "" { 317 intervalType = p.Type 318 } 319 return &bq.TimePartitioning{ 320 Type: string(intervalType), 321 ExpirationMs: int64(p.Expiration / time.Millisecond), 322 Field: p.Field, 323 RequirePartitionFilter: p.RequirePartitionFilter, 324 } 325} 326 327func bqToTimePartitioning(q *bq.TimePartitioning) *TimePartitioning { 328 if q == nil { 329 return nil 330 } 331 return &TimePartitioning{ 332 Type: TimePartitioningType(q.Type), 333 Expiration: time.Duration(q.ExpirationMs) * time.Millisecond, 334 Field: q.Field, 335 RequirePartitionFilter: q.RequirePartitionFilter, 336 } 337} 338 339// RangePartitioning indicates an integer-range based storage organization strategy. 340type RangePartitioning struct { 341 // The field by which the table is partitioned. 342 // This field must be a top-level field, and must be typed as an 343 // INTEGER/INT64. 344 Field string 345 // The details of how partitions are mapped onto the integer range. 346 Range *RangePartitioningRange 347} 348 349// RangePartitioningRange defines the boundaries and width of partitioned values. 350type RangePartitioningRange struct { 351 // The start value of defined range of values, inclusive of the specified value. 352 Start int64 353 // The end of the defined range of values, exclusive of the defined value. 354 End int64 355 // The width of each interval range. 356 Interval int64 357} 358 359func (rp *RangePartitioning) toBQ() *bq.RangePartitioning { 360 if rp == nil { 361 return nil 362 } 363 return &bq.RangePartitioning{ 364 Field: rp.Field, 365 Range: rp.Range.toBQ(), 366 } 367} 368 369func bqToRangePartitioning(q *bq.RangePartitioning) *RangePartitioning { 370 if q == nil { 371 return nil 372 } 373 return &RangePartitioning{ 374 Field: q.Field, 375 Range: bqToRangePartitioningRange(q.Range), 376 } 377} 378 379func bqToRangePartitioningRange(br *bq.RangePartitioningRange) *RangePartitioningRange { 380 if br == nil { 381 return nil 382 } 383 return &RangePartitioningRange{ 384 Start: br.Start, 385 End: br.End, 386 Interval: br.Interval, 387 } 388} 389 390func (rpr *RangePartitioningRange) toBQ() *bq.RangePartitioningRange { 391 if rpr == nil { 392 return nil 393 } 394 return &bq.RangePartitioningRange{ 395 Start: rpr.Start, 396 End: rpr.End, 397 Interval: rpr.Interval, 398 ForceSendFields: []string{"Start", "End", "Interval"}, 399 } 400} 401 402// Clustering governs the organization of data within a managed table. 403// For more information, see https://cloud.google.com/bigquery/docs/clustered-tables 404type Clustering struct { 405 Fields []string 406} 407 408func (c *Clustering) toBQ() *bq.Clustering { 409 if c == nil { 410 return nil 411 } 412 return &bq.Clustering{ 413 Fields: c.Fields, 414 } 415} 416 417func bqToClustering(q *bq.Clustering) *Clustering { 418 if q == nil { 419 return nil 420 } 421 return &Clustering{ 422 Fields: q.Fields, 423 } 424} 425 426// EncryptionConfig configures customer-managed encryption on tables and ML models. 427type EncryptionConfig struct { 428 // Describes the Cloud KMS encryption key that will be used to protect 429 // destination BigQuery table. The BigQuery Service Account associated with your 430 // project requires access to this encryption key. 431 KMSKeyName string 432} 433 434func (e *EncryptionConfig) toBQ() *bq.EncryptionConfiguration { 435 if e == nil { 436 return nil 437 } 438 return &bq.EncryptionConfiguration{ 439 KmsKeyName: e.KMSKeyName, 440 } 441} 442 443func bqToEncryptionConfig(q *bq.EncryptionConfiguration) *EncryptionConfig { 444 if q == nil { 445 return nil 446 } 447 return &EncryptionConfig{ 448 KMSKeyName: q.KmsKeyName, 449 } 450} 451 452// StreamingBuffer holds information about the streaming buffer. 453type StreamingBuffer struct { 454 // A lower-bound estimate of the number of bytes currently in the streaming 455 // buffer. 456 EstimatedBytes uint64 457 458 // A lower-bound estimate of the number of rows currently in the streaming 459 // buffer. 460 EstimatedRows uint64 461 462 // The time of the oldest entry in the streaming buffer. 463 OldestEntryTime time.Time 464} 465 466func (t *Table) toBQ() *bq.TableReference { 467 return &bq.TableReference{ 468 ProjectId: t.ProjectID, 469 DatasetId: t.DatasetID, 470 TableId: t.TableID, 471 } 472} 473 474// FullyQualifiedName returns the ID of the table in projectID:datasetID.tableID format. 475func (t *Table) FullyQualifiedName() string { 476 return fmt.Sprintf("%s:%s.%s", t.ProjectID, t.DatasetID, t.TableID) 477} 478 479// implicitTable reports whether Table is an empty placeholder, which signifies that a new table should be created with an auto-generated Table ID. 480func (t *Table) implicitTable() bool { 481 return t.ProjectID == "" && t.DatasetID == "" && t.TableID == "" 482} 483 484// Create creates a table in the BigQuery service. 485// Pass in a TableMetadata value to configure the table. 486// If tm.View.Query is non-empty, the created table will be of type VIEW. 487// If no ExpirationTime is specified, the table will never expire. 488// After table creation, a view can be modified only if its table was initially created 489// with a view. 490func (t *Table) Create(ctx context.Context, tm *TableMetadata) (err error) { 491 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Create") 492 defer func() { trace.EndSpan(ctx, err) }() 493 494 table, err := tm.toBQ() 495 if err != nil { 496 return err 497 } 498 table.TableReference = &bq.TableReference{ 499 ProjectId: t.ProjectID, 500 DatasetId: t.DatasetID, 501 TableId: t.TableID, 502 } 503 req := t.c.bqs.Tables.Insert(t.ProjectID, t.DatasetID, table).Context(ctx) 504 setClientHeader(req.Header()) 505 _, err = req.Do() 506 return err 507} 508 509func (tm *TableMetadata) toBQ() (*bq.Table, error) { 510 t := &bq.Table{} 511 if tm == nil { 512 return t, nil 513 } 514 if tm.Schema != nil && tm.ViewQuery != "" { 515 return nil, errors.New("bigquery: provide Schema or ViewQuery, not both") 516 } 517 t.FriendlyName = tm.Name 518 t.Description = tm.Description 519 t.Labels = tm.Labels 520 if tm.Schema != nil { 521 t.Schema = tm.Schema.toBQ() 522 } 523 if tm.ViewQuery != "" { 524 if tm.UseStandardSQL && tm.UseLegacySQL { 525 return nil, errors.New("bigquery: cannot provide both UseStandardSQL and UseLegacySQL") 526 } 527 t.View = &bq.ViewDefinition{Query: tm.ViewQuery} 528 if tm.UseLegacySQL { 529 t.View.UseLegacySql = true 530 } else { 531 t.View.UseLegacySql = false 532 t.View.ForceSendFields = append(t.View.ForceSendFields, "UseLegacySql") 533 } 534 } else if tm.UseLegacySQL || tm.UseStandardSQL { 535 return nil, errors.New("bigquery: UseLegacy/StandardSQL requires ViewQuery") 536 } 537 t.MaterializedView = tm.MaterializedView.toBQ() 538 t.TimePartitioning = tm.TimePartitioning.toBQ() 539 t.RangePartitioning = tm.RangePartitioning.toBQ() 540 t.Clustering = tm.Clustering.toBQ() 541 t.RequirePartitionFilter = tm.RequirePartitionFilter 542 t.SnapshotDefinition = tm.SnapshotDefinition.toBQ() 543 544 if !validExpiration(tm.ExpirationTime) { 545 return nil, fmt.Errorf("invalid expiration time: %v.\n"+ 546 "Valid expiration times are after 1678 and before 2262", tm.ExpirationTime) 547 } 548 if !tm.ExpirationTime.IsZero() && tm.ExpirationTime != NeverExpire { 549 t.ExpirationTime = tm.ExpirationTime.UnixNano() / 1e6 550 } 551 if tm.ExternalDataConfig != nil { 552 edc := tm.ExternalDataConfig.toBQ() 553 t.ExternalDataConfiguration = &edc 554 } 555 t.EncryptionConfiguration = tm.EncryptionConfig.toBQ() 556 if tm.FullID != "" { 557 return nil, errors.New("cannot set FullID on create") 558 } 559 if tm.Type != "" { 560 return nil, errors.New("cannot set Type on create") 561 } 562 if !tm.CreationTime.IsZero() { 563 return nil, errors.New("cannot set CreationTime on create") 564 } 565 if !tm.LastModifiedTime.IsZero() { 566 return nil, errors.New("cannot set LastModifiedTime on create") 567 } 568 if tm.NumBytes != 0 { 569 return nil, errors.New("cannot set NumBytes on create") 570 } 571 if tm.NumLongTermBytes != 0 { 572 return nil, errors.New("cannot set NumLongTermBytes on create") 573 } 574 if tm.NumRows != 0 { 575 return nil, errors.New("cannot set NumRows on create") 576 } 577 if tm.StreamingBuffer != nil { 578 return nil, errors.New("cannot set StreamingBuffer on create") 579 } 580 if tm.ETag != "" { 581 return nil, errors.New("cannot set ETag on create") 582 } 583 return t, nil 584} 585 586// Metadata fetches the metadata for the table. 587func (t *Table) Metadata(ctx context.Context) (md *TableMetadata, err error) { 588 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Metadata") 589 defer func() { trace.EndSpan(ctx, err) }() 590 591 req := t.c.bqs.Tables.Get(t.ProjectID, t.DatasetID, t.TableID).Context(ctx) 592 setClientHeader(req.Header()) 593 var table *bq.Table 594 err = runWithRetry(ctx, func() (err error) { 595 table, err = req.Do() 596 return err 597 }) 598 if err != nil { 599 return nil, err 600 } 601 return bqToTableMetadata(table, t.c) 602} 603 604func bqToTableMetadata(t *bq.Table, c *Client) (*TableMetadata, error) { 605 md := &TableMetadata{ 606 Description: t.Description, 607 Name: t.FriendlyName, 608 Location: t.Location, 609 Type: TableType(t.Type), 610 FullID: t.Id, 611 Labels: t.Labels, 612 NumBytes: t.NumBytes, 613 NumLongTermBytes: t.NumLongTermBytes, 614 NumRows: t.NumRows, 615 ExpirationTime: unixMillisToTime(t.ExpirationTime), 616 CreationTime: unixMillisToTime(t.CreationTime), 617 LastModifiedTime: unixMillisToTime(int64(t.LastModifiedTime)), 618 ETag: t.Etag, 619 EncryptionConfig: bqToEncryptionConfig(t.EncryptionConfiguration), 620 RequirePartitionFilter: t.RequirePartitionFilter, 621 SnapshotDefinition: bqToSnapshotDefinition(t.SnapshotDefinition, c), 622 } 623 if t.MaterializedView != nil { 624 md.MaterializedView = bqToMaterializedViewDefinition(t.MaterializedView) 625 } 626 if t.Schema != nil { 627 md.Schema = bqToSchema(t.Schema) 628 } 629 if t.View != nil { 630 md.ViewQuery = t.View.Query 631 md.UseLegacySQL = t.View.UseLegacySql 632 } 633 md.TimePartitioning = bqToTimePartitioning(t.TimePartitioning) 634 md.RangePartitioning = bqToRangePartitioning(t.RangePartitioning) 635 md.Clustering = bqToClustering(t.Clustering) 636 if t.StreamingBuffer != nil { 637 md.StreamingBuffer = &StreamingBuffer{ 638 EstimatedBytes: t.StreamingBuffer.EstimatedBytes, 639 EstimatedRows: t.StreamingBuffer.EstimatedRows, 640 OldestEntryTime: unixMillisToTime(int64(t.StreamingBuffer.OldestEntryTime)), 641 } 642 } 643 if t.ExternalDataConfiguration != nil { 644 edc, err := bqToExternalDataConfig(t.ExternalDataConfiguration) 645 if err != nil { 646 return nil, err 647 } 648 md.ExternalDataConfig = edc 649 } 650 return md, nil 651} 652 653// Delete deletes the table. 654func (t *Table) Delete(ctx context.Context) (err error) { 655 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Delete") 656 defer func() { trace.EndSpan(ctx, err) }() 657 658 call := t.c.bqs.Tables.Delete(t.ProjectID, t.DatasetID, t.TableID).Context(ctx) 659 setClientHeader(call.Header()) 660 661 return runWithRetry(ctx, func() (err error) { 662 err = call.Do() 663 return err 664 }) 665} 666 667// Read fetches the contents of the table. 668func (t *Table) Read(ctx context.Context) *RowIterator { 669 return t.read(ctx, fetchPage) 670} 671 672func (t *Table) read(ctx context.Context, pf pageFetcher) *RowIterator { 673 return newRowIterator(ctx, &rowSource{t: t}, pf) 674} 675 676// NeverExpire is a sentinel value used to remove a table'e expiration time. 677var NeverExpire = time.Time{}.Add(-1) 678 679// Update modifies specific Table metadata fields. 680func (t *Table) Update(ctx context.Context, tm TableMetadataToUpdate, etag string) (md *TableMetadata, err error) { 681 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Update") 682 defer func() { trace.EndSpan(ctx, err) }() 683 684 bqt, err := tm.toBQ() 685 if err != nil { 686 return nil, err 687 } 688 call := t.c.bqs.Tables.Patch(t.ProjectID, t.DatasetID, t.TableID, bqt).Context(ctx) 689 setClientHeader(call.Header()) 690 if etag != "" { 691 call.Header().Set("If-Match", etag) 692 } 693 var res *bq.Table 694 if err := runWithRetry(ctx, func() (err error) { 695 res, err = call.Do() 696 return err 697 }); err != nil { 698 return nil, err 699 } 700 return bqToTableMetadata(res, t.c) 701} 702 703func (tm *TableMetadataToUpdate) toBQ() (*bq.Table, error) { 704 t := &bq.Table{} 705 forceSend := func(field string) { 706 t.ForceSendFields = append(t.ForceSendFields, field) 707 } 708 709 if tm.Description != nil { 710 t.Description = optional.ToString(tm.Description) 711 forceSend("Description") 712 } 713 if tm.Name != nil { 714 t.FriendlyName = optional.ToString(tm.Name) 715 forceSend("FriendlyName") 716 } 717 if tm.MaterializedView != nil { 718 t.MaterializedView = tm.MaterializedView.toBQ() 719 forceSend("MaterializedView") 720 } 721 if tm.Schema != nil { 722 t.Schema = tm.Schema.toBQ() 723 forceSend("Schema") 724 } 725 if tm.EncryptionConfig != nil { 726 t.EncryptionConfiguration = tm.EncryptionConfig.toBQ() 727 } 728 729 if tm.Clustering != nil { 730 t.Clustering = tm.Clustering.toBQ() 731 } 732 733 if !validExpiration(tm.ExpirationTime) { 734 return nil, invalidTimeError(tm.ExpirationTime) 735 } 736 if tm.ExpirationTime == NeverExpire { 737 t.NullFields = append(t.NullFields, "ExpirationTime") 738 } else if !tm.ExpirationTime.IsZero() { 739 t.ExpirationTime = tm.ExpirationTime.UnixNano() / 1e6 740 forceSend("ExpirationTime") 741 } 742 if tm.TimePartitioning != nil { 743 t.TimePartitioning = tm.TimePartitioning.toBQ() 744 t.TimePartitioning.ForceSendFields = []string{"RequirePartitionFilter"} 745 if tm.TimePartitioning.Expiration == 0 { 746 t.TimePartitioning.NullFields = []string{"ExpirationMs"} 747 } 748 } 749 if tm.RequirePartitionFilter != nil { 750 t.RequirePartitionFilter = optional.ToBool(tm.RequirePartitionFilter) 751 forceSend("RequirePartitionFilter") 752 } 753 if tm.ViewQuery != nil { 754 t.View = &bq.ViewDefinition{ 755 Query: optional.ToString(tm.ViewQuery), 756 ForceSendFields: []string{"Query"}, 757 } 758 } 759 if tm.UseLegacySQL != nil { 760 if t.View == nil { 761 t.View = &bq.ViewDefinition{} 762 } 763 t.View.UseLegacySql = optional.ToBool(tm.UseLegacySQL) 764 t.View.ForceSendFields = append(t.View.ForceSendFields, "UseLegacySql") 765 } 766 labels, forces, nulls := tm.update() 767 t.Labels = labels 768 t.ForceSendFields = append(t.ForceSendFields, forces...) 769 t.NullFields = append(t.NullFields, nulls...) 770 return t, nil 771} 772 773// validExpiration ensures a specified time is either the sentinel NeverExpire, 774// the zero value, or within the defined range of UnixNano. Internal 775// represetations of expiration times are based upon Time.UnixNano. Any time 776// before 1678 or after 2262 cannot be represented by an int64 and is therefore 777// undefined and invalid. See https://godoc.org/time#Time.UnixNano. 778func validExpiration(t time.Time) bool { 779 return t == NeverExpire || t.IsZero() || time.Unix(0, t.UnixNano()).Equal(t) 780} 781 782// invalidTimeError emits a consistent error message for failures of the 783// validExpiration function. 784func invalidTimeError(t time.Time) error { 785 return fmt.Errorf("invalid expiration time %v. "+ 786 "Valid expiration times are after 1678 and before 2262", t) 787} 788 789// TableMetadataToUpdate is used when updating a table's metadata. 790// Only non-nil fields will be updated. 791type TableMetadataToUpdate struct { 792 // The user-friendly description of this table. 793 Description optional.String 794 795 // The user-friendly name for this table. 796 Name optional.String 797 798 // The table's schema. 799 // When updating a schema, you can add columns but not remove them. 800 Schema Schema 801 802 // The table's clustering configuration. 803 // For more information on how modifying clustering affects the table, see: 804 // https://cloud.google.com/bigquery/docs/creating-clustered-tables#modifying-cluster-spec 805 Clustering *Clustering 806 807 // The table's encryption configuration. 808 EncryptionConfig *EncryptionConfig 809 810 // The time when this table expires. To remove a table's expiration, 811 // set ExpirationTime to NeverExpire. The zero value is ignored. 812 ExpirationTime time.Time 813 814 // The query to use for a view. 815 ViewQuery optional.String 816 817 // Use Legacy SQL for the view query. 818 UseLegacySQL optional.Bool 819 820 // MaterializedView allows changes to the underlying materialized view 821 // definition. When calling Update, ensure that all mutable fields of 822 // MaterializedViewDefinition are populated. 823 MaterializedView *MaterializedViewDefinition 824 825 // TimePartitioning allows modification of certain aspects of partition 826 // configuration such as partition expiration and whether partition 827 // filtration is required at query time. When calling Update, ensure 828 // that all mutable fields of TimePartitioning are populated. 829 TimePartitioning *TimePartitioning 830 831 // RequirePartitionFilter governs whether the table enforces partition 832 // elimination when referenced in a query. 833 RequirePartitionFilter optional.Bool 834 835 labelUpdater 836} 837 838// labelUpdater contains common code for updating labels. 839type labelUpdater struct { 840 setLabels map[string]string 841 deleteLabels map[string]bool 842} 843 844// SetLabel causes a label to be added or modified on a call to Update. 845func (u *labelUpdater) SetLabel(name, value string) { 846 if u.setLabels == nil { 847 u.setLabels = map[string]string{} 848 } 849 u.setLabels[name] = value 850} 851 852// DeleteLabel causes a label to be deleted on a call to Update. 853func (u *labelUpdater) DeleteLabel(name string) { 854 if u.deleteLabels == nil { 855 u.deleteLabels = map[string]bool{} 856 } 857 u.deleteLabels[name] = true 858} 859 860func (u *labelUpdater) update() (labels map[string]string, forces, nulls []string) { 861 if u.setLabels == nil && u.deleteLabels == nil { 862 return nil, nil, nil 863 } 864 labels = map[string]string{} 865 for k, v := range u.setLabels { 866 labels[k] = v 867 } 868 if len(labels) == 0 && len(u.deleteLabels) > 0 { 869 forces = []string{"Labels"} 870 } 871 for l := range u.deleteLabels { 872 nulls = append(nulls, "Labels."+l) 873 } 874 return labels, forces, nulls 875} 876