// Copyright 2015 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package bigquery import ( "fmt" "reflect" "testing" "time" bq "google.golang.org/api/bigquery/v2" ) func (fs *FieldSchema) GoString() string { if fs == nil { return "" } return fmt.Sprintf("{Name:%s Description:%s Repeated:%t Required:%t Type:%s Schema:%s}", fs.Name, fs.Description, fs.Repeated, fs.Required, fs.Type, fmt.Sprintf("%#v", fs.Schema), ) } func bqTableFieldSchema(desc, name, typ, mode string) *bq.TableFieldSchema { return &bq.TableFieldSchema{ Description: desc, Name: name, Mode: mode, Type: typ, } } func fieldSchema(desc, name, typ string, repeated, required bool) *FieldSchema { return &FieldSchema{ Description: desc, Name: name, Repeated: repeated, Required: required, Type: FieldType(typ), } } func TestSchemaConversion(t *testing.T) { testCases := []struct { schema Schema bqSchema *bq.TableSchema }{ { // required bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "STRING", "REQUIRED"), }, }, schema: Schema{ fieldSchema("desc", "name", "STRING", false, true), }, }, { // repeated bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "STRING", "REPEATED"), }, }, schema: Schema{ fieldSchema("desc", "name", "STRING", true, false), }, }, { // nullable, string bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "STRING", ""), }, }, schema: Schema{ fieldSchema("desc", "name", "STRING", false, false), }, }, { // integer bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "INTEGER", ""), }, }, schema: Schema{ fieldSchema("desc", "name", "INTEGER", false, false), }, }, { // float bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "FLOAT", ""), }, }, schema: Schema{ fieldSchema("desc", "name", "FLOAT", false, false), }, }, { // boolean bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "BOOLEAN", ""), }, }, schema: Schema{ fieldSchema("desc", "name", "BOOLEAN", false, false), }, }, { // timestamp bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("desc", "name", "TIMESTAMP", ""), }, }, schema: Schema{ fieldSchema("desc", "name", "TIMESTAMP", false, false), }, }, { // nested bqSchema: &bq.TableSchema{ Fields: []*bq.TableFieldSchema{ { Description: "An outer schema wrapping a nested schema", Name: "outer", Mode: "REQUIRED", Type: "RECORD", Fields: []*bq.TableFieldSchema{ bqTableFieldSchema("inner field", "inner", "STRING", ""), }, }, }, }, schema: Schema{ &FieldSchema{ Description: "An outer schema wrapping a nested schema", Name: "outer", Required: true, Type: "RECORD", Schema: []*FieldSchema{ { Description: "inner field", Name: "inner", Type: "STRING", }, }, }, }, }, } for _, tc := range testCases { bqSchema := tc.schema.asTableSchema() if !reflect.DeepEqual(bqSchema, tc.bqSchema) { t.Errorf("converting to TableSchema: got:\n%v\nwant:\n%v", bqSchema, tc.bqSchema) } schema := convertTableSchema(tc.bqSchema) if !reflect.DeepEqual(schema, tc.schema) { t.Errorf("converting to Schema: got:\n%v\nwant:\n%v", schema, tc.schema) } } } type allStrings struct { String string ByteSlice []byte } type allSignedIntegers struct { Int64 int64 Int32 int32 Int16 int16 Int8 int8 Int int } type allUnsignedIntegers struct { Uint64 uint64 Uint32 uint32 Uint16 uint16 Uint8 uint8 Uintptr uintptr Uint uint } type allFloat struct { Float64 float64 Float32 float32 // NOTE: Complex32 and Complex64 are unsupported by BigQuery } type allBoolean struct { Bool bool } type allTime struct { Time time.Time } func TestSimpleInference(t *testing.T) { testCases := []struct { in interface{} want Schema }{ { in: allSignedIntegers{}, want: Schema{ fieldSchema("", "Int64", "INTEGER", false, true), fieldSchema("", "Int32", "INTEGER", false, true), fieldSchema("", "Int16", "INTEGER", false, true), fieldSchema("", "Int8", "INTEGER", false, true), fieldSchema("", "Int", "INTEGER", false, true), }, }, { in: allUnsignedIntegers{}, want: Schema{ fieldSchema("", "Uint64", "INTEGER", false, true), fieldSchema("", "Uint32", "INTEGER", false, true), fieldSchema("", "Uint16", "INTEGER", false, true), fieldSchema("", "Uint8", "INTEGER", false, true), fieldSchema("", "Uintptr", "INTEGER", false, true), fieldSchema("", "Uint", "INTEGER", false, true), }, }, { in: allFloat{}, want: Schema{ fieldSchema("", "Float64", "FLOAT", false, true), fieldSchema("", "Float32", "FLOAT", false, true), }, }, { in: allBoolean{}, want: Schema{ fieldSchema("", "Bool", "BOOLEAN", false, true), }, }, { in: allTime{}, want: Schema{ fieldSchema("", "Time", "TIMESTAMP", false, true), }, }, { in: allStrings{}, want: Schema{ fieldSchema("", "String", "STRING", false, true), fieldSchema("", "ByteSlice", "STRING", false, true), }, }, } for i, tc := range testCases { got, err := InferSchema(tc.in) if err != nil { t.Fatalf("%d: error inferring TableSchema: %v", i, err) } if !reflect.DeepEqual(got, tc.want) { t.Errorf("%d: inferring TableSchema: got:\n%#v\nwant:\n%#v", i, got, tc.want) } } } type containsNested struct { hidden string NotNested int Nested struct { Inside int } } type containsDoubleNested struct { NotNested int Nested struct { InsideNested struct { Inside int } } } func TestNestedInference(t *testing.T) { testCases := []struct { in interface{} want Schema }{ { in: containsNested{}, want: Schema{ fieldSchema("", "NotNested", "INTEGER", false, true), &FieldSchema{ Name: "Nested", Required: true, Type: "RECORD", Schema: []*FieldSchema{ { Name: "Inside", Type: "INTEGER", Required: true, }, }, }, }, }, { in: containsDoubleNested{}, want: Schema{ fieldSchema("", "NotNested", "INTEGER", false, true), &FieldSchema{ Name: "Nested", Required: true, Type: "RECORD", Schema: []*FieldSchema{ { Name: "InsideNested", Required: true, Type: "RECORD", Schema: []*FieldSchema{ { Name: "Inside", Type: "INTEGER", Required: true, }, }, }, }, }, }, }, } for i, tc := range testCases { got, err := InferSchema(tc.in) if err != nil { t.Fatalf("%d: error inferring TableSchema: %v", i, err) } if !reflect.DeepEqual(got, tc.want) { t.Errorf("%d: inferring TableSchema: got:\n%#v\nwant:\n%#v", i, got, tc.want) } } } type simpleRepeated struct { NotRepeated []byte RepeatedByteSlice [][]byte Repeated []int } type simpleNestedRepeated struct { NotRepeated int Repeated []struct { Inside int } } func TestRepeatedInference(t *testing.T) { testCases := []struct { in interface{} want Schema }{ { in: simpleRepeated{}, want: Schema{ fieldSchema("", "NotRepeated", "STRING", false, true), fieldSchema("", "RepeatedByteSlice", "STRING", true, false), fieldSchema("", "Repeated", "INTEGER", true, false), }, }, { in: simpleNestedRepeated{}, want: Schema{ fieldSchema("", "NotRepeated", "INTEGER", false, true), &FieldSchema{ Name: "Repeated", Repeated: true, Type: "RECORD", Schema: []*FieldSchema{ { Name: "Inside", Type: "INTEGER", Required: true, }, }, }, }, }, } for i, tc := range testCases { got, err := InferSchema(tc.in) if err != nil { t.Fatalf("%d: error inferring TableSchema: %v", i, err) } if !reflect.DeepEqual(got, tc.want) { t.Errorf("%d: inferring TableSchema: got:\n%#v\nwant:\n%#v", i, got, tc.want) } } } type Embedded struct { Embedded int } type nestedEmbedded struct { Embedded } func TestSchemaErrors(t *testing.T) { testCases := []struct { in interface{} err error }{ { in: []byte{}, err: errNoStruct, }, { in: new(int), err: errNoStruct, }, { in: new(allStrings), err: errNoStruct, }, { in: struct{ Complex complex64 }{}, err: errUnsupportedFieldType, }, { in: struct{ Map map[string]int }{}, err: errUnsupportedFieldType, }, { in: struct{ Chan chan bool }{}, err: errUnsupportedFieldType, }, { in: struct{ Ptr *int }{}, err: errUnsupportedFieldType, }, { in: struct{ Interface interface{} }{}, err: errUnsupportedFieldType, }, { in: struct{ MultiDimensional [][]int }{}, err: errUnsupportedFieldType, }, { in: struct{ MultiDimensional [][][]byte }{}, err: errUnsupportedFieldType, }, { in: struct{ ChanSlice []chan bool }{}, err: errUnsupportedFieldType, }, { in: struct{ NestedChan struct{ Chan []chan bool } }{}, err: errUnsupportedFieldType, }, { in: nestedEmbedded{}, err: errUnsupportedFieldType, }, } for i, tc := range testCases { want := tc.err _, got := InferSchema(tc.in) if !reflect.DeepEqual(got, want) { t.Errorf("%d: inferring TableSchema: got:\n%#v\nwant:\n%#v", i, got, want) } } }