From 3f32d4c403f8ad2851cbdd07190a13a03e5ce4a5 Mon Sep 17 00:00:00 2001 From: The Magician Date: Tue, 12 Sep 2023 12:39:43 -0400 Subject: [PATCH] Add BigQuery Table Constraints Field (#8885) (#15815) * Add BigQuery Table Constraints Field * fix indentation for resource_bigquery_table_test Signed-off-by: Modular Magician --- .changelog/8885.txt | 3 + .../bigquery/resource_bigquery_table.go | 290 ++++++++++++++++++ .../bigquery/resource_bigquery_table_test.go | 277 +++++++++++++++++ website/docs/r/bigquery_table.html.markdown | 48 +++ 4 files changed, 618 insertions(+) create mode 100644 .changelog/8885.txt diff --git a/.changelog/8885.txt b/.changelog/8885.txt new file mode 100644 index 00000000000..53b91eede1d --- /dev/null +++ b/.changelog/8885.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +bigquery: added `table_constraints` field to `google_bigquery_table` resource +``` diff --git a/google/services/bigquery/resource_bigquery_table.go b/google/services/bigquery/resource_bigquery_table.go index 90369d83941..f9c5e31c6d4 100644 --- a/google/services/bigquery/resource_bigquery_table.go +++ b/google/services/bigquery/resource_bigquery_table.go @@ -1071,6 +1071,119 @@ func ResourceBigQueryTable() *schema.Resource { Default: true, Description: `Whether or not to allow Terraform to destroy the instance. Unless this field is set to false in Terraform state, a terraform destroy or terraform apply that would delete the instance will fail.`, }, + + // TableConstraints: [Optional] Defines the primary key and foreign keys. + "table_constraints": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `Defines the primary key and foreign keys.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // PrimaryKey: [Optional] Represents the primary key constraint + // on a table's columns. Present only if the table has a primary key. + // The primary key is not enforced. + "primary_key": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `Represents a primary key constraint on a table's columns. Present only if the table has a primary key. The primary key is not enforced.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + //Columns: [Required] The columns that are composed of the primary key constraint. + "columns": { + Type: schema.TypeList, + Required: true, + Description: `The columns that are composed of the primary key constraint.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + }, + + // ForeignKeys: [Optional] Present only if the table has a foreign key. + // The foreign key is not enforced. + "foreign_keys": { + Type: schema.TypeList, + Optional: true, + Description: `Present only if the table has a foreign key. The foreign key is not enforced.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // Name: [Optional] Set only if the foreign key constraint is named. + "name": { + Type: schema.TypeString, + Optional: true, + Description: `Set only if the foreign key constraint is named.`, + }, + + // ReferencedTable: [Required] The table that holds the primary key + // and is referenced by this foreign key. + "referenced_table": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Description: `The table that holds the primary key and is referenced by this foreign key.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // ProjectId: [Required] The ID of the project containing this table. + "project_id": { + Type: schema.TypeString, + Required: true, + Description: `The ID of the project containing this table.`, + }, + + // DatasetId: [Required] The ID of the dataset containing this table. + "dataset_id": { + Type: schema.TypeString, + Required: true, + Description: `The ID of the dataset containing this table.`, + }, + + // TableId: [Required] The ID of the table. The ID must contain only + // letters (a-z, A-Z), numbers (0-9), or underscores (_). The maximum + // length is 1,024 characters. Certain operations allow suffixing of + // the table ID with a partition decorator, such as + // sample_table$20190123. + "table_id": { + Type: schema.TypeString, + Required: true, + Description: `The ID of the table. The ID must contain only letters (a-z, A-Z), numbers (0-9), or underscores (_). The maximum length is 1,024 characters. Certain operations allow suffixing of the table ID with a partition decorator, such as sample_table$20190123.`, + }, + }, + }, + }, + + // ColumnReferences: [Required] The pair of the foreign key column and primary key column. + "column_references": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Description: `The pair of the foreign key column and primary key column.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // ReferencingColumn: [Required] The column that composes the foreign key. + "referencing_column": { + Type: schema.TypeString, + Required: true, + Description: `The column that composes the foreign key.`, + }, + + // ReferencedColumn: [Required] The column in the primary key that are + // referenced by the referencingColumn + "referenced_column": { + Type: schema.TypeString, + Required: true, + Description: `The column in the primary key that are referenced by the referencingColumn.`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, UseJSONNumber: true, } @@ -1172,6 +1285,15 @@ func resourceTable(d *schema.ResourceData, meta interface{}) (*bigquery.Table, e } } + if v, ok := d.GetOk("table_constraints"); ok { + tableConstraints, err := expandTableConstraints(v) + if err != nil { + return nil, err + } + + table.TableConstraints = tableConstraints + } + return table, nil } @@ -1383,6 +1505,14 @@ func resourceBigQueryTableRead(d *schema.ResourceData, meta interface{}) error { } } + if res.TableConstraints != nil { + table_constraints := flattenTableConstraints(res.TableConstraints) + + if err := d.Set("table_constraints", table_constraints); err != nil { + return fmt.Errorf("Error setting table constraints: %s", err) + } + } + return nil } @@ -2009,6 +2139,166 @@ func flattenMaterializedView(mvd *bigquery.MaterializedViewDefinition) []map[str return []map[string]interface{}{result} } +func expandPrimaryKey(configured interface{}) *bigquery.TableConstraintsPrimaryKey { + if len(configured.([]interface{})) == 0 { + return nil + } + + raw := configured.([]interface{})[0].(map[string]interface{}) + pk := &bigquery.TableConstraintsPrimaryKey{} + + columns := []string{} + for _, rawColumn := range raw["columns"].([]interface{}) { + columns = append(columns, rawColumn.(string)) + } + if len(columns) > 0 { + pk.Columns = columns + } + + return pk +} + +func flattenPrimaryKey(edc *bigquery.TableConstraintsPrimaryKey) []map[string]interface{} { + result := map[string]interface{}{} + + if edc.Columns != nil { + result["columns"] = edc.Columns + } + + return []map[string]interface{}{result} +} + +func expandReferencedTable(configured interface{}) *bigquery.TableConstraintsForeignKeysReferencedTable { + raw := configured.([]interface{})[0].(map[string]interface{}) + rt := &bigquery.TableConstraintsForeignKeysReferencedTable{} + + if v, ok := raw["project_id"]; ok { + rt.ProjectId = v.(string) + } + if v, ok := raw["dataset_id"]; ok { + rt.DatasetId = v.(string) + } + if v, ok := raw["table_id"]; ok { + rt.TableId = v.(string) + } + + return rt +} + +func flattenReferencedTable(edc *bigquery.TableConstraintsForeignKeysReferencedTable) []map[string]interface{} { + result := map[string]interface{}{} + + result["project_id"] = edc.ProjectId + result["dataset_id"] = edc.DatasetId + result["table_id"] = edc.TableId + + return []map[string]interface{}{result} +} + +func expandColumnReference(configured interface{}) *bigquery.TableConstraintsForeignKeysColumnReferences { + raw := configured.(map[string]interface{}) + + cr := &bigquery.TableConstraintsForeignKeysColumnReferences{} + + if v, ok := raw["referencing_column"]; ok { + cr.ReferencingColumn = v.(string) + } + if v, ok := raw["referenced_column"]; ok { + cr.ReferencedColumn = v.(string) + } + + return cr +} + +func flattenColumnReferences(edc []*bigquery.TableConstraintsForeignKeysColumnReferences) []map[string]interface{} { + results := []map[string]interface{}{} + + for _, cr := range edc { + result := map[string]interface{}{} + result["referenced_column"] = cr.ReferencedColumn + result["referencing_column"] = cr.ReferencingColumn + results = append(results, result) + } + + return results +} + +func expandForeignKey(configured interface{}) *bigquery.TableConstraintsForeignKeys { + raw := configured.(map[string]interface{}) + + fk := &bigquery.TableConstraintsForeignKeys{} + if v, ok := raw["name"]; ok { + fk.Name = v.(string) + } + if v, ok := raw["referenced_table"]; ok { + fk.ReferencedTable = expandReferencedTable(v) + } + crs := []*bigquery.TableConstraintsForeignKeysColumnReferences{} + if v, ok := raw["column_references"]; ok { + for _, rawColumnReferences := range v.([]interface{}) { + crs = append(crs, expandColumnReference(rawColumnReferences)) + } + } + + if len(crs) > 0 { + fk.ColumnReferences = crs + } + + return fk +} + +func flattenForeignKeys(edc []*bigquery.TableConstraintsForeignKeys) []map[string]interface{} { + results := []map[string]interface{}{} + + for _, fr := range edc { + result := map[string]interface{}{} + result["name"] = fr.Name + result["column_references"] = flattenColumnReferences(fr.ColumnReferences) + result["referenced_table"] = flattenReferencedTable(fr.ReferencedTable) + results = append(results, result) + } + + return results +} + +func expandTableConstraints(cfg interface{}) (*bigquery.TableConstraints, error) { + raw := cfg.([]interface{})[0].(map[string]interface{}) + + edc := &bigquery.TableConstraints{} + + if v, ok := raw["primary_key"]; ok { + edc.PrimaryKey = expandPrimaryKey(v) + } + + fks := []*bigquery.TableConstraintsForeignKeys{} + + if v, ok := raw["foreign_keys"]; ok { + for _, rawForeignKey := range v.([]interface{}) { + fks = append(fks, expandForeignKey(rawForeignKey)) + } + } + + if len(fks) > 0 { + edc.ForeignKeys = fks + } + + return edc, nil + +} + +func flattenTableConstraints(edc *bigquery.TableConstraints) []map[string]interface{} { + result := map[string]interface{}{} + + if edc.PrimaryKey != nil { + result["primary_key"] = flattenPrimaryKey(edc.PrimaryKey) + } + if edc.ForeignKeys != nil { + result["foreign_keys"] = flattenForeignKeys(edc.ForeignKeys) + } + + return []map[string]interface{}{result} +} + func resourceBigQueryTableImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { config := meta.(*transport_tpg.Config) if err := tpgresource.ParseImportId([]string{ diff --git a/google/services/bigquery/resource_bigquery_table_test.go b/google/services/bigquery/resource_bigquery_table_test.go index a5fca4a0592..290282522d9 100644 --- a/google/services/bigquery/resource_bigquery_table_test.go +++ b/google/services/bigquery/resource_bigquery_table_test.go @@ -300,6 +300,93 @@ func TestAccBigQueryTable_RangePartitioning(t *testing.T) { }) } +func TestAccBigQueryTable_PrimaryKey(t *testing.T) { + t.Parallel() + resourceName := "google_bigquery_table.test" + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTablePrimaryKey(datasetID, tableID), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccBigQueryTable_ForeignKey(t *testing.T) { + t.Parallel() + resourceName := "google_bigquery_table.test" + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID_pk := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID_fk := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + + projectID := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTableForeignKeys(projectID, datasetID, tableID_pk, tableID_fk), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccBigQueryTable_updateTableConstraints(t *testing.T) { + t.Parallel() + resourceName := "google_bigquery_table.test" + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID_pk := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID_fk := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + + projectID := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTableForeignKeys(projectID, datasetID, tableID_pk, tableID_fk), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccBigQueryTableTableConstraintsUpdate(projectID, datasetID, tableID_pk, tableID_fk), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func TestAccBigQueryTable_View(t *testing.T) { t.Parallel() @@ -3059,6 +3146,196 @@ resource "google_bigquery_table" "test" { `, datasetID, tableID) } +func testAccBigQueryTablePrimaryKey(datasetID, tableID string) string { + return fmt.Sprintf(` + resource "google_bigquery_dataset" "foo" { + dataset_id = "%s" + } + + resource "google_bigquery_table" "test" { + deletion_protection = false + table_id = "%s" + dataset_id = google_bigquery_dataset.foo.dataset_id + + table_constraints { + primary_key { + columns = ["id"] + } + } + + schema = <The `external_data_configuration` block supports: * `autodetect` - (Required) - Let BigQuery try to autodetect the schema @@ -366,6 +369,51 @@ in Terraform state, a `terraform destroy` or `terraform apply` that would delete `google_bigquery_default_service_account` datasource and the `google_kms_crypto_key_iam_binding` resource. +The `table_constraints` block supports: + +* `primary_key` - (Optional) Represents the primary key constraint + on a table's columns. Present only if the table has a primary key. + The primary key is not enforced. + Structure is [documented below](#nested_primary_key). + +* `foreign_keys` - (Optional) Present only if the table has a foreign key. + The foreign key is not enforced. + Structure is [documented below](#nested_foreign_keys). + +The `primary_key` block supports: + +* `columns`: (Required) The columns that are composed of the primary key constraint. + +The `foreign_keys` block supports: + +* `name`: (Optional) Set only if the foreign key constraint is named. + +* `referenced_table`: (Required) The table that holds the primary key + and is referenced by this foreign key. + Structure is [documented below](#nested_referenced_table). + +* `column_references`: (Required) The pair of the foreign key column and primary key column. + Structure is [documented below](#nested_column_references). + +The `referenced_table` block supports: + +* `project_id`: (Required) The ID of the project containing this table. + +* `dataset_id`: (Required) The ID of the dataset containing this table. + +* `table_id`: (Required) The ID of the table. The ID must contain only + letters (a-z, A-Z), numbers (0-9), or underscores (_). The maximum + length is 1,024 characters. Certain operations allow suffixing of + the table ID with a partition decorator, such as + sample_table$20190123. + +The `column_references` block supports: + +* `referencing_column`: (Required) The column that composes the foreign key. + +* `referenced_column`: (Required) The column in the primary key that are + referenced by the referencingColumn + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are