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