bigquery: Add support for removing an expiration

Prior to this change, calls to Update did not support removing a table's
expiration, even though the REST API supports such an operation.

Because the TableUpdateMetadata ExpirationTime field is a time.Time
value, rather than a pointer to a time.Time, we cannot pass in nil to
indicate the expiration time should be removed. Instead, this change
uses a sentinel value, the zero value time.Time minus one second, and
advises callers to use the NeverExpire variable added in this commit.

Fixes #1196.

Change-Id: Ib2cae9bc7744da75e86c62f8d476e78b6e8d6e73
Reviewed-on: https://code-review.googlesource.com/c/34631
Reviewed-by: Jean de Klerk <deklerk@google.com>
diff --git a/bigquery/table.go b/bigquery/table.go
index b7553e7..52d1af0 100644
--- a/bigquery/table.go
+++ b/bigquery/table.go
@@ -69,8 +69,9 @@
 	// Clustering specifies the data clustering configuration for the table.
 	Clustering *Clustering
 
-	// The time when this table expires. If not set, the table will persist
-	// indefinitely. Expired tables will be deleted and their storage reclaimed.
+	// The time when this table expires. If set, this table will expire at the
+	// specified time. Expired tables will be deleted and their storage
+	// reclaimed. The zero value is ignored.
 	ExpirationTime time.Time
 
 	// User-provided labels.
@@ -278,7 +279,7 @@
 // Create creates a table in the BigQuery service.
 // Pass in a TableMetadata value to configure the table.
 // If tm.View.Query is non-empty, the created table will be of type VIEW.
-// Expiration can only be set during table creation.
+// If no ExpirationTime is specified, the table will never expire.
 // After table creation, a view can be modified only if its table was initially created
 // with a view.
 func (t *Table) Create(ctx context.Context, tm *TableMetadata) (err error) {
@@ -443,6 +444,10 @@
 	return newRowIterator(ctx, t, pf)
 }
 
+// NeverExpire is a sentinel value used with TableMetadataToUpdate's
+// ExpirationTime to indicate that a table should never expire.
+var NeverExpire = time.Time{}.Add(-1)
+
 // Update modifies specific Table metadata fields.
 func (t *Table) Update(ctx context.Context, tm TableMetadataToUpdate, etag string) (md *TableMetadata, err error) {
 	ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Table.Update")
@@ -485,7 +490,9 @@
 	if tm.EncryptionConfig != nil {
 		t.EncryptionConfiguration = tm.EncryptionConfig.toBQ()
 	}
-	if !tm.ExpirationTime.IsZero() {
+	if tm.ExpirationTime == NeverExpire {
+		t.NullFields = append(t.NullFields, "ExpirationTime")
+	} else if !tm.ExpirationTime.IsZero() {
 		t.ExpirationTime = tm.ExpirationTime.UnixNano() / 1e6
 		forceSend("ExpirationTime")
 	}
@@ -530,7 +537,8 @@
 	// all mutable fields of EncryptionConfig are populated.
 	EncryptionConfig *EncryptionConfig
 
-	// The time when this table expires.
+	// The time when this table expires. To remove a table's expiration,
+	// set ExpirationTime to NeverExpire. The zero value is ignored.
 	ExpirationTime time.Time
 
 	// The query to use for a view.
diff --git a/bigquery/table_test.go b/bigquery/table_test.go
index 0c4d926..84e2e8f 100644
--- a/bigquery/table_test.go
+++ b/bigquery/table_test.go
@@ -298,6 +298,12 @@
 				NullFields: []string{"Labels.D"},
 			},
 		},
+		{
+			tm: TableMetadataToUpdate{ExpirationTime: NeverExpire},
+			want: &bq.Table{
+				NullFields: []string{"ExpirationTime"},
+			},
+		},
 	} {
 		got := test.tm.toBQ()
 		if !testutil.Equal(got, test.want) {