Repository: googleapis/google-cloud-go
Pull request #5672 (2022-02-18 04:32:40)

feat(bigquery): support table cloning
This PR adds support for cloning tables.


Patch:
diff --git a/bigquery/copy.go b/bigquery/copy.go
index aaec924113..a8226e9798 100644
--- a/bigquery/copy.go
+++ b/bigquery/copy.go
@@ -28,10 +28,14 @@ type TableCopyOperationType string
 var (
 	// CopyOperation indicates normal table to table copying.
 	CopyOperation TableCopyOperationType = "COPY"
-	// SnapshotOperation indicates creating a snapshot from a regular table.
+	// SnapshotOperation indicates creating a snapshot from a regular table, which
+	// operates as an immutable copy.
 	SnapshotOperation TableCopyOperationType = "SNAPSHOT"
 	// RestoreOperation indicates creating/restoring a table from a snapshot.
 	RestoreOperation TableCopyOperationType = "RESTORE"
+	// CloneOperation indicates creating a table clone, which creates a writeable
+	// copy of a base table that is billed based on difference from the base table.
+	CloneOperation TableCopyOperationType = "CLONE"
 )
 
 // CopyConfig holds the configuration for a copy job.
diff --git a/bigquery/table.go b/bigquery/table.go
index 52dbd14ad2..2a67fa3971 100644
--- a/bigquery/table.go
+++ b/bigquery/table.go
@@ -124,6 +124,10 @@ type TableMetadata struct {
 	// given snapshot table.
 	SnapshotDefinition *SnapshotDefinition
 
+	// CloneDefinition contains additional information about the provenance of a
+	// given cloned table.
+	CloneDefinition *CloneDefinition
+
 	// Contains information regarding this table's streaming buffer, if one is
 	// present. This field will be nil if the table is not being streamed to or if
 	// there is no data in the streaming buffer.
@@ -266,6 +270,42 @@ func bqToSnapshotDefinition(q *bq.SnapshotDefinition, c *Client) *SnapshotDefini
 	return sd
 }
 
+// CloneDefinition provides metadata related to the origin of a clone.
+type CloneDefinition struct {
+
+	// BaseTableReference describes the ID of the table that this clone
+	// came from.
+	BaseTableReference *Table
+
+	// CloneTime indicates when the base table was cloned.
+	CloneTime time.Time
+}
+
+func (cd *CloneDefinition) toBQ() *bq.CloneDefinition {
+	if cd == nil {
+		return nil
+	}
+	return &bq.CloneDefinition{
+		BaseTableReference: cd.BaseTableReference.toBQ(),
+		CloneTime:          cd.CloneTime.Format(time.RFC3339),
+	}
+}
+
+func bqToCloneDefinition(q *bq.CloneDefinition, c *Client) *CloneDefinition {
+	if q == nil {
+		return nil
+	}
+	cd := &CloneDefinition{
+		BaseTableReference: bqToTable(q.BaseTableReference, c),
+	}
+	// It's possible we could fail to populate CloneTime if we fail to parse
+	// the backend representation.
+	if t, err := time.Parse(time.RFC3339, q.CloneTime); err == nil {
+		cd.CloneTime = t
+	}
+	return cd
+}
+
 // TimePartitioningType defines the interval used to partition managed data.
 type TimePartitioningType string
 
@@ -580,6 +620,7 @@ func (tm *TableMetadata) toBQ() (*bq.Table, error) {
 	t.Clustering = tm.Clustering.toBQ()
 	t.RequirePartitionFilter = tm.RequirePartitionFilter
 	t.SnapshotDefinition = tm.SnapshotDefinition.toBQ()
+	t.CloneDefinition = tm.CloneDefinition.toBQ()
 
 	if !validExpiration(tm.ExpirationTime) {
 		return nil, fmt.Errorf("invalid expiration time: %v.\n"+
@@ -659,6 +700,7 @@ func bqToTableMetadata(t *bq.Table, c *Client) (*TableMetadata, error) {
 		EncryptionConfig:       bqToEncryptionConfig(t.EncryptionConfiguration),
 		RequirePartitionFilter: t.RequirePartitionFilter,
 		SnapshotDefinition:     bqToSnapshotDefinition(t.SnapshotDefinition, c),
+		CloneDefinition:        bqToCloneDefinition(t.CloneDefinition, c),
 	}
 	if t.MaterializedView != nil {
 		md.MaterializedView = bqToMaterializedViewDefinition(t.MaterializedView)


Test patch:
diff --git a/bigquery/integration_test.go b/bigquery/integration_test.go
index c5a05fd77a..5505170fef 100644
--- a/bigquery/integration_test.go
+++ b/bigquery/integration_test.go
@@ -322,7 +322,7 @@ func TestIntegration_JobFrom(t *testing.T) {
 
 }
 
-func TestIntegration_SnapshotAndRestore(t *testing.T) {
+func TestIntegration_SnapshotRestoreClone(t *testing.T) {
 
 	if client == nil {
 		t.Skip("Integration tests skipped")
@@ -344,7 +344,7 @@ func TestIntegration_SnapshotAndRestore(t *testing.T) {
 		CONCAT("group", CAST(CAST(RAND()*10 AS INT64) AS STRING))
 		FROM
 		UNNEST(GENERATE_ARRAY(0,999))
-`, qualified)
+		`, qualified)
 	if _, _, err := runQuerySQL(ctx, sql); err != nil {
 		t.Fatalf("couldn't instantiate base table: %v", err)
 	}
@@ -403,7 +403,47 @@ func TestIntegration_SnapshotAndRestore(t *testing.T) {
 	if meta.NumRows != restoreMeta.NumRows {
 		t.Errorf("row counts mismatch.  snap had %d rows, restore had %d rows", meta.NumRows, restoreMeta.NumRows)
 	}
+	if restoreMeta.Type != RegularTable {
+		t.Errorf("table type mismatch, got %s want %s", restoreMeta.Type, RegularTable)
+	}
+
+	// Create a clone of the snapshot.
+	cloneID := tableIDs.New()
+	cloner := dataset.Table(cloneID).CopierFrom(dataset.Table(snapshotID))
+	cloner.OperationType = CloneOperation
 
+	job, err = cloner.Run(ctx)
+	if err != nil {
+		t.Fatalf("couldn't run clone: %v", err)
+	}
+	err = wait(ctx, job)
+	if err != nil {
+		t.Fatalf("clone failed: %v", err)
+	}
+
+	cloneMeta, err := dataset.Table(cloneID).Metadata(ctx)
+	if err != nil {
+		t.Fatalf("couldn't get restored table metadata: %v", err)
+	}
+	if meta.NumBytes != cloneMeta.NumBytes {
+		t.Errorf("bytes mismatch.  snap had %d bytes, clone had %d bytes", meta.NumBytes, cloneMeta.NumBytes)
+	}
+	if meta.NumRows != cloneMeta.NumRows {
+		t.Errorf("row counts mismatch.  snap had %d rows, clone had %d rows", meta.NumRows, cloneMeta.NumRows)
+	}
+	if cloneMeta.Type != RegularTable {
+		t.Errorf("table type mismatch, got %s want %s", cloneMeta.Type, RegularTable)
+	}
+	if cloneMeta.CloneDefinition == nil {
+		t.Errorf("expected CloneDefinition in (%q), was nil", cloneMeta.FullID)
+	}
+	if cloneMeta.CloneDefinition.BaseTableReference == nil {
+		t.Errorf("expected CloneDefinition.BaseTableReference, was nil")
+	}
+	wantBase := dataset.Table(snapshotID)
+	if !testutil.Equal(cloneMeta.CloneDefinition.BaseTableReference, wantBase, cmpopts.IgnoreUnexported(Table{})) {
+		t.Errorf("mismatch in CloneDefinition.BaseTableReference.  Got %s, want %s", cloneMeta.CloneDefinition.BaseTableReference.FullyQualifiedName(), wantBase.FullyQualifiedName())
+	}
 }
 
 func TestIntegration_HourTimePartitioning(t *testing.T) {