blob: 1c481ef8313afd5c14f7427a9e13d87b0da48b84 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package bigquery
import (
"encoding/json"
"fmt"
"testing"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
)
func TestBigQueryTableSchemaDiffSuppress(t *testing.T) {
t.Parallel()
cases := map[string]struct {
Old, New string
ExpectDiffSuppress bool
}{
"empty schema": {
Old: "null",
New: "[]",
ExpectDiffSuppress: true,
},
"empty schema -> non-empty": {
Old: "null",
New: `[
{
"name": "PageNo",
"type": "INTEGER"
}
]`,
ExpectDiffSuppress: false,
},
"no change": {
Old: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"finalKey\" : {} }]",
New: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"finalKey\" : {} }]",
ExpectDiffSuppress: true,
},
"remove key": {
Old: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"finalKey\" : {} }]",
New: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"finalKey\" : {} }]",
ExpectDiffSuppress: false,
},
"empty description -> default description (empty)": {
Old: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"description\": \"\" }]",
New: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\" }]",
ExpectDiffSuppress: true,
},
"empty description -> other description": {
Old: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"description\": \"\" }]",
New: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"description\": \"somethingRandom\" }]",
ExpectDiffSuppress: false,
},
"mode NULLABLE -> other mode": {
Old: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"mode\": \"NULLABLE\" }]",
New: "[{\"name\": \"someValue\", \"type\": \"INT64\", \"anotherKey\" : \"anotherValue\", \"mode\": \"somethingRandom\" }]",
ExpectDiffSuppress: false,
},
"mode NULLABLE -> default mode (also NULLABLE)": {
Old: `[
{
"mode": "NULLABLE",
"name": "PageNo",
"type": "INTEGER"
}
]`,
New: `[
{
"name": "PageNo",
"type": "INTEGER"
}
]`,
ExpectDiffSuppress: true,
},
"mode & type uppercase -> lowercase": {
Old: `[
{
"mode": "NULLABLE",
"name": "PageNo",
"type": "INTEGER"
}
]`,
New: `[
{
"mode": "nullable",
"name": "PageNo",
"type": "integer"
}
]`,
ExpectDiffSuppress: true,
},
"type INTEGER -> INT64": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"INTEGER\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"INT64\" }]",
ExpectDiffSuppress: true,
},
"type INTEGER -> other": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"INTEGER\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"somethingRandom\" }]",
ExpectDiffSuppress: false,
},
"type FLOAT -> FLOAT64": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"FLOAT\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"FLOAT64\" }]",
ExpectDiffSuppress: true,
},
"type FLOAT -> other": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"FLOAT\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"somethingRandom\" }]",
ExpectDiffSuppress: false,
},
"type BOOLEAN -> BOOL": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOLEAN\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOL\" }]",
ExpectDiffSuppress: true,
},
"type BOOLEAN -> other": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOLEAN\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"somethingRandom\" }]",
ExpectDiffSuppress: false,
},
// this is invalid but we need to make sure we don't cause a panic
// if users provide an invalid schema
"invalid - missing type for old": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOLEAN\" }]",
ExpectDiffSuppress: false,
},
// this is invalid but we need to make sure we don't cause a panic
// if users provide an invalid schema
"invalid - missing type for new": {
Old: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOLEAN\" }]",
New: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\" }]",
ExpectDiffSuppress: false,
},
"reordering fields": {
Old: `[
{
"name": "PageNo",
"type": "INTEGER"
},
{
"name": "IngestTime",
"type": "TIMESTAMP"
}
]`,
New: `[
{
"name": "IngestTime",
"type": "TIMESTAMP"
},
{
"name": "PageNo",
"type": "INTEGER"
}
]`,
ExpectDiffSuppress: true,
},
"reordering fields with value change": {
Old: `[
{
"name": "PageNo",
"type": "INTEGER",
"description": "someVal"
},
{
"name": "IngestTime",
"type": "TIMESTAMP"
}
]`,
New: `[
{
"name": "IngestTime",
"type": "TIMESTAMP"
},
{
"name": "PageNo",
"type": "INTEGER",
"description": "otherVal"
}
]`,
ExpectDiffSuppress: false,
},
"nested field ordering changes": {
Old: `[
{
"name": "someValue",
"type": "INTEGER",
"fields": [
{
"name": "value1",
"type": "INTEGER",
"mode": "NULLABLE",
"description": "someVal"
},
{
"name": "value2",
"type": "BOOLEAN",
"mode": "NULLABLE",
"description": "someVal"
}
]
}
]`,
New: `[
{
"name": "someValue",
"type": "INTEGER",
"fields": [
{
"name": "value2",
"type": "BOOLEAN",
"mode": "NULLABLE",
"description": "someVal"
},
{
"name": "value1",
"type": "INTEGER",
"mode": "NULLABLE",
"description": "someVal"
}
]
}
]`,
ExpectDiffSuppress: true,
},
"policyTags": {
Old: `[
{
"mode": "NULLABLE",
"name": "providerphone",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
},
"type":"STRING"
}
]`,
New: `[
{
"name": "providerphone",
"type": "STRING",
"policyTags": {
"names": ["projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"]
}
}
]`,
ExpectDiffSuppress: true,
},
"multiple levels of reordering with policyTags set": {
Old: `[
{
"mode": "NULLABLE",
"name": "providerphone",
"type":"STRING",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
},
"fields": [
{
"name": "value1",
"type": "INTEGER",
"mode": "NULLABLE",
"description": "someVal",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
}
},
{
"name": "value2",
"type": "BOOLEAN",
"mode": "NULLABLE",
"description": "someVal"
}
]
},
{
"name": "PageNo",
"type": "INTEGER"
},
{
"name": "IngestTime",
"type": "TIMESTAMP",
"fields": [
{
"name": "value3",
"type": "INTEGER",
"mode": "NULLABLE",
"description": "someVal",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
}
},
{
"name": "value4",
"type": "BOOLEAN",
"mode": "NULLABLE",
"description": "someVal"
}
]
}
]`,
New: `[
{
"name": "IngestTime",
"type": "TIMESTAMP",
"fields": [
{
"name": "value4",
"type": "BOOLEAN",
"mode": "NULLABLE",
"description": "someVal"
},
{
"name": "value3",
"type": "INTEGER",
"mode": "NULLABLE",
"description": "someVal",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
}
}
]
},
{
"mode": "NULLABLE",
"name": "providerphone",
"type":"STRING",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
},
"fields": [
{
"name": "value1",
"type": "INTEGER",
"mode": "NULLABLE",
"description": "someVal",
"policyTags": {
"names": [
"projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"
]
}
},
{
"name": "value2",
"type": "BOOLEAN",
"mode": "NULLABLE",
"description": "someVal"
}
]
},
{
"name": "PageNo",
"type": "INTEGER"
}
]`,
ExpectDiffSuppress: true,
},
}
for tn, tc := range cases {
tn := tn
tc := tc
t.Run(tn, func(t *testing.T) {
t.Parallel()
var a, b interface{}
if err := json.Unmarshal([]byte(tc.Old), &a); err != nil {
t.Fatalf(fmt.Sprintf("unable to unmarshal old json - %v", err))
}
if err := json.Unmarshal([]byte(tc.New), &b); err != nil {
t.Fatalf(fmt.Sprintf("unable to unmarshal new json - %v", err))
}
if bigQueryTableSchemaDiffSuppress("schema", tc.Old, tc.New, nil) != tc.ExpectDiffSuppress {
t.Fatalf("bad: %s, %q => %q expect DiffSuppress to return %t", tn, tc.Old, tc.New, tc.ExpectDiffSuppress)
}
})
}
}
type testUnitBigQueryDataTableJSONChangeableTestCase struct {
name string
jsonOld string
jsonNew string
isExternalTable bool
changeable bool
}
func (testcase *testUnitBigQueryDataTableJSONChangeableTestCase) check(t *testing.T) {
var old, new interface{}
if err := json.Unmarshal([]byte(testcase.jsonOld), &old); err != nil {
t.Fatalf("unable to unmarshal json - %v", err)
}
if err := json.Unmarshal([]byte(testcase.jsonNew), &new); err != nil {
t.Fatalf("unable to unmarshal json - %v", err)
}
changeable, err := resourceBigQueryTableSchemaIsChangeable(old, new, testcase.isExternalTable, true)
if err != nil {
t.Errorf("%s failed unexpectedly: %s", testcase.name, err)
}
if changeable != testcase.changeable {
t.Errorf("expected changeable result of %v but got %v for testcase %s", testcase.changeable, changeable, testcase.name)
}
d := &tpgresource.ResourceDiffMock{
Before: map[string]interface{}{},
After: map[string]interface{}{},
}
d.Before["schema"] = testcase.jsonOld
d.After["schema"] = testcase.jsonNew
if testcase.isExternalTable {
d.Before["external_data_configuration"] = ""
d.After["external_data_configuration"] = ""
}
err = resourceBigQueryTableSchemaCustomizeDiffFunc(d)
if err != nil {
t.Errorf("error on testcase %s - %v", testcase.name, err)
}
if !testcase.changeable != d.IsForceNew {
t.Errorf("%s: expected d.IsForceNew to be %v, but was %v", testcase.name, !testcase.changeable, d.IsForceNew)
}
}
var testUnitBigQueryDataTableIsChangeableTestCases = []testUnitBigQueryDataTableJSONChangeableTestCase{
{
name: "defaultEquality",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
changeable: true,
},
{
name: "arraySizeIncreases",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }, {\"name\": \"asomeValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
changeable: true,
},
{
name: "arraySizeDecreases",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }, {\"name\": \"asomeValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
changeable: true,
},
{
name: "externalArraySizeDecreases",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }, {\"name\": \"asomeValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
isExternalTable: true,
changeable: false,
},
{
name: "descriptionChanges",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "typeInteger",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"INT64\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "typeFloat",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"FLOAT\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"FLOAT64\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "typeBool",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"BOOL\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "typeChangeIncompatible",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"DATETIME\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: false,
},
// this is invalid but we need to make sure we don't cause a panic
// if users provide an invalid schema
{
name: "typeChangeIgnoreNewMissingType",
jsonOld: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\" }]",
jsonNew: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOLEAN\" }]",
changeable: true,
},
// this is invalid but we need to make sure we don't cause a panic
// if users provide an invalid schema
{
name: "typeChangeIgnoreOldMissingType",
jsonOld: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\" }]",
jsonNew: "[{\"name\": \"someValue\", \"anotherKey\" : \"anotherValue\", \"type\": \"BOOLEAN\" }]",
changeable: true,
},
{
name: "typeModeReqToNull",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"REQUIRED\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "typeModeIncompatible",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"REQUIRED\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"REPEATED\", \"description\" : \"some new value\" }]",
changeable: false,
},
{
name: "modeToDefaultNullable",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"REQUIRED\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "orderOfArrayChangesAndDescriptionChanges",
jsonOld: "[{\"name\": \"value1\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }, {\"name\": \"value2\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"value2\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"newVal\" }, {\"name\": \"value1\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
changeable: true,
},
{
name: "orderOfArrayChangesAndNameChanges",
jsonOld: "[{\"name\": \"value1\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }, {\"name\": \"value2\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"value3\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"newVal\" }, {\"name\": \"value1\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
changeable: false,
},
{
name: "renameRequiredColumn",
jsonOld: "[{\"name\": \"value1\", \"type\" : \"INTEGER\", \"mode\" : \"REQUIRED\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"value3\", \"type\" : \"INTEGER\", \"mode\" : \"REQUIRED\", \"description\" : \"someVal\" }]",
changeable: false,
},
{
name: "renameNullableColumn",
jsonOld: "[{\"name\": \"value1\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"value3\", \"type\" : \"INTEGER\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
changeable: false,
},
{
name: "typeModeReqToNullAndColumnDropped",
jsonOld: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"REQUIRED\", \"description\" : \"someVal\" }, {\"name\": \"someValue2\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"someVal\" }]",
jsonNew: "[{\"name\": \"someValue\", \"type\" : \"BOOLEAN\", \"mode\" : \"NULLABLE\", \"description\" : \"some new value\" }]",
changeable: true,
},
{
name: "policyTags",
jsonOld: `[
{
"mode": "NULLABLE",
"name": "providerphone",
"policyTags": {
"names": ["projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"]
},
"type":"STRING"
}
]`,
jsonNew: `[
{
"name": "providerphone",
"type": "STRING",
"policyTags": {
"names": ["projects/my-project/locations/us/taxonomies/12345678/policyTags/12345678"]
}
}
]`,
changeable: true,
},
}
func TestUnitBigQueryDataTable_schemaIsChangeable(t *testing.T) {
t.Parallel()
for _, testcase := range testUnitBigQueryDataTableIsChangeableTestCases {
testcase.check(t)
}
}
func TestUnitBigQueryDataTable_schemaIsChangeableNested(t *testing.T) {
t.Parallel()
// Only top level column drops are changeable
customNestedValues := map[string]bool{"arraySizeDecreases": false, "typeModeReqToNullAndColumnDropped": false}
for _, testcase := range testUnitBigQueryDataTableIsChangeableTestCases {
changeable := testcase.changeable
if overrideValue, ok := customNestedValues[testcase.name]; ok {
changeable = overrideValue
}
testcaseNested := &testUnitBigQueryDataTableJSONChangeableTestCase{
testcase.name + "Nested",
fmt.Sprintf("[{\"name\": \"someValue\", \"type\" : \"INTEGER\", \"fields\" : %s }]", testcase.jsonOld),
fmt.Sprintf("[{\"name\": \"someValue\", \"type\" : \"INT64\", \"fields\" : %s }]", testcase.jsonNew),
testcase.isExternalTable,
changeable,
}
testcaseNested.check(t)
}
}