2018-06-19 15:30:26 +02:00
// Copyright 2015 Google LLC
//
// 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"
"math/big"
"reflect"
"testing"
"time"
"cloud.google.com/go/civil"
"cloud.google.com/go/internal/pretty"
"cloud.google.com/go/internal/testutil"
bq "google.golang.org/api/bigquery/v2"
)
func ( fs * FieldSchema ) GoString ( ) string {
if fs == nil {
return "<nil>"
}
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 ) ,
} ,
} ,
{
// civil times
bqSchema : & bq . TableSchema {
Fields : [ ] * bq . TableFieldSchema {
bqTableFieldSchema ( "desc" , "f1" , "TIME" , "" ) ,
bqTableFieldSchema ( "desc" , "f2" , "DATE" , "" ) ,
bqTableFieldSchema ( "desc" , "f3" , "DATETIME" , "" ) ,
} ,
} ,
schema : Schema {
fieldSchema ( "desc" , "f1" , "TIME" , false , false ) ,
fieldSchema ( "desc" , "f2" , "DATE" , false , false ) ,
fieldSchema ( "desc" , "f3" , "DATETIME" , false , false ) ,
} ,
} ,
{
// numeric
bqSchema : & bq . TableSchema {
Fields : [ ] * bq . TableFieldSchema {
bqTableFieldSchema ( "desc" , "n" , "NUMERIC" , "" ) ,
} ,
} ,
schema : Schema {
fieldSchema ( "desc" , "n" , "NUMERIC" , false , false ) ,
} ,
} ,
2019-03-17 20:19:56 +01:00
{
bqSchema : & bq . TableSchema {
Fields : [ ] * bq . TableFieldSchema {
bqTableFieldSchema ( "geo" , "g" , "GEOGRAPHY" , "" ) ,
} ,
} ,
schema : Schema {
fieldSchema ( "geo" , "g" , "GEOGRAPHY" , false , false ) ,
} ,
} ,
2018-06-19 15:30:26 +02:00
{
// 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 : Schema {
{
Description : "inner field" ,
Name : "inner" ,
Type : "STRING" ,
} ,
} ,
} ,
} ,
} ,
}
for _ , tc := range testCases {
bqSchema := tc . schema . toBQ ( )
if ! testutil . Equal ( bqSchema , tc . bqSchema ) {
t . Errorf ( "converting to TableSchema: got:\n%v\nwant:\n%v" ,
pretty . Value ( bqSchema ) , pretty . Value ( tc . bqSchema ) )
}
schema := bqToSchema ( tc . bqSchema )
if ! testutil . Equal ( 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 {
Uint32 uint32
Uint16 uint16
Uint8 uint8
}
type allFloat struct {
Float64 float64
Float32 float32
// NOTE: Complex32 and Complex64 are unsupported by BigQuery
}
type allBoolean struct {
Bool bool
}
type allTime struct {
Timestamp time . Time
Time civil . Time
Date civil . Date
DateTime civil . DateTime
}
type allNumeric struct {
Numeric * big . Rat
}
func reqField ( name , typ string ) * FieldSchema {
return & FieldSchema {
Name : name ,
Type : FieldType ( typ ) ,
Required : true ,
}
}
func optField ( name , typ string ) * FieldSchema {
return & FieldSchema {
Name : name ,
Type : FieldType ( typ ) ,
Required : false ,
}
}
func TestSimpleInference ( t * testing . T ) {
testCases := [ ] struct {
in interface { }
want Schema
} {
{
in : allSignedIntegers { } ,
want : Schema {
reqField ( "Int64" , "INTEGER" ) ,
reqField ( "Int32" , "INTEGER" ) ,
reqField ( "Int16" , "INTEGER" ) ,
reqField ( "Int8" , "INTEGER" ) ,
reqField ( "Int" , "INTEGER" ) ,
} ,
} ,
{
in : allUnsignedIntegers { } ,
want : Schema {
reqField ( "Uint32" , "INTEGER" ) ,
reqField ( "Uint16" , "INTEGER" ) ,
reqField ( "Uint8" , "INTEGER" ) ,
} ,
} ,
{
in : allFloat { } ,
want : Schema {
reqField ( "Float64" , "FLOAT" ) ,
reqField ( "Float32" , "FLOAT" ) ,
} ,
} ,
{
in : allBoolean { } ,
want : Schema {
reqField ( "Bool" , "BOOLEAN" ) ,
} ,
} ,
{
in : & allBoolean { } ,
want : Schema {
reqField ( "Bool" , "BOOLEAN" ) ,
} ,
} ,
{
in : allTime { } ,
want : Schema {
reqField ( "Timestamp" , "TIMESTAMP" ) ,
reqField ( "Time" , "TIME" ) ,
reqField ( "Date" , "DATE" ) ,
reqField ( "DateTime" , "DATETIME" ) ,
} ,
} ,
{
in : & allNumeric { } ,
want : Schema {
reqField ( "Numeric" , "NUMERIC" ) ,
} ,
} ,
{
in : allStrings { } ,
want : Schema {
reqField ( "String" , "STRING" ) ,
reqField ( "ByteSlice" , "BYTES" ) ,
} ,
} ,
}
for _ , tc := range testCases {
got , err := InferSchema ( tc . in )
if err != nil {
t . Fatalf ( "%T: error inferring TableSchema: %v" , tc . in , err )
}
if ! testutil . Equal ( got , tc . want ) {
t . Errorf ( "%T: inferring TableSchema: got:\n%#v\nwant:\n%#v" , tc . in ,
pretty . Value ( got ) , pretty . Value ( tc . want ) )
}
}
}
type containsNested struct {
NotNested int
Nested struct {
Inside int
}
}
type containsDoubleNested struct {
NotNested int
Nested struct {
InsideNested struct {
Inside int
}
}
}
type ptrNested struct {
Ptr * struct { Inside int }
}
type dup struct { // more than one field of the same struct type
A , B allBoolean
}
func TestNestedInference ( t * testing . T ) {
testCases := [ ] struct {
in interface { }
want Schema
} {
{
in : containsNested { } ,
want : Schema {
reqField ( "NotNested" , "INTEGER" ) ,
& FieldSchema {
Name : "Nested" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Inside" , "INTEGER" ) } ,
} ,
} ,
} ,
{
in : containsDoubleNested { } ,
want : Schema {
reqField ( "NotNested" , "INTEGER" ) ,
& FieldSchema {
Name : "Nested" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema {
{
Name : "InsideNested" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Inside" , "INTEGER" ) } ,
} ,
} ,
} ,
} ,
} ,
{
in : ptrNested { } ,
want : Schema {
& FieldSchema {
Name : "Ptr" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Inside" , "INTEGER" ) } ,
} ,
} ,
} ,
{
in : dup { } ,
want : Schema {
& FieldSchema {
Name : "A" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Bool" , "BOOLEAN" ) } ,
} ,
& FieldSchema {
Name : "B" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Bool" , "BOOLEAN" ) } ,
} ,
} ,
} ,
}
for _ , tc := range testCases {
got , err := InferSchema ( tc . in )
if err != nil {
t . Fatalf ( "%T: error inferring TableSchema: %v" , tc . in , err )
}
if ! testutil . Equal ( got , tc . want ) {
t . Errorf ( "%T: inferring TableSchema: got:\n%#v\nwant:\n%#v" , tc . in ,
pretty . Value ( got ) , pretty . Value ( tc . want ) )
}
}
}
type repeated struct {
NotRepeated [ ] byte
RepeatedByteSlice [ ] [ ] byte
Slice [ ] int
Array [ 5 ] bool
}
type nestedRepeated struct {
NotRepeated int
Repeated [ ] struct {
Inside int
}
RepeatedPtr [ ] * struct { Inside int }
}
func repField ( name , typ string ) * FieldSchema {
return & FieldSchema {
Name : name ,
Type : FieldType ( typ ) ,
Repeated : true ,
}
}
func TestRepeatedInference ( t * testing . T ) {
testCases := [ ] struct {
in interface { }
want Schema
} {
{
in : repeated { } ,
want : Schema {
reqField ( "NotRepeated" , "BYTES" ) ,
repField ( "RepeatedByteSlice" , "BYTES" ) ,
repField ( "Slice" , "INTEGER" ) ,
repField ( "Array" , "BOOLEAN" ) ,
} ,
} ,
{
in : nestedRepeated { } ,
want : Schema {
reqField ( "NotRepeated" , "INTEGER" ) ,
{
Name : "Repeated" ,
Repeated : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Inside" , "INTEGER" ) } ,
} ,
{
Name : "RepeatedPtr" ,
Repeated : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "Inside" , "INTEGER" ) } ,
} ,
} ,
} ,
}
for i , tc := range testCases {
got , err := InferSchema ( tc . in )
if err != nil {
t . Fatalf ( "%d: error inferring TableSchema: %v" , i , err )
}
if ! testutil . Equal ( got , tc . want ) {
t . Errorf ( "%d: inferring TableSchema: got:\n%#v\nwant:\n%#v" , i ,
pretty . Value ( got ) , pretty . Value ( tc . want ) )
}
}
}
type allNulls struct {
A NullInt64
B NullFloat64
C NullBool
D NullString
E NullTimestamp
F NullTime
G NullDate
H NullDateTime
2019-03-17 20:19:56 +01:00
I NullGeography
2018-06-19 15:30:26 +02:00
}
func TestNullInference ( t * testing . T ) {
got , err := InferSchema ( allNulls { } )
if err != nil {
t . Fatal ( err )
}
want := Schema {
optField ( "A" , "INTEGER" ) ,
optField ( "B" , "FLOAT" ) ,
optField ( "C" , "BOOLEAN" ) ,
optField ( "D" , "STRING" ) ,
optField ( "E" , "TIMESTAMP" ) ,
optField ( "F" , "TIME" ) ,
optField ( "G" , "DATE" ) ,
optField ( "H" , "DATETIME" ) ,
2019-03-17 20:19:56 +01:00
optField ( "I" , "GEOGRAPHY" ) ,
2018-06-19 15:30:26 +02:00
}
if diff := testutil . Diff ( got , want ) ; diff != "" {
t . Error ( diff )
}
}
type Embedded struct {
Embedded int
}
type embedded struct {
Embedded2 int
}
type nestedEmbedded struct {
Embedded
embedded
}
func TestEmbeddedInference ( t * testing . T ) {
got , err := InferSchema ( nestedEmbedded { } )
if err != nil {
t . Fatal ( err )
}
want := Schema {
reqField ( "Embedded" , "INTEGER" ) ,
reqField ( "Embedded2" , "INTEGER" ) ,
}
if ! testutil . Equal ( got , want ) {
t . Errorf ( "got %v, want %v" , pretty . Value ( got ) , pretty . Value ( want ) )
}
}
func TestRecursiveInference ( t * testing . T ) {
type List struct {
Val int
Next * List
}
_ , err := InferSchema ( List { } )
if err == nil {
t . Fatal ( "got nil, want error" )
}
}
type withTags struct {
NoTag int
ExcludeTag int ` bigquery:"-" `
SimpleTag int ` bigquery:"simple_tag" `
UnderscoreTag int ` bigquery:"_id" `
MixedCase int ` bigquery:"MIXEDcase" `
Nullable [ ] byte ` bigquery:",nullable" `
NullNumeric * big . Rat ` bigquery:",nullable" `
}
type withTagsNested struct {
Nested withTags ` bigquery:"nested" `
NestedAnonymous struct {
ExcludeTag int ` bigquery:"-" `
Inside int ` bigquery:"inside" `
} ` bigquery:"anon" `
PNested * struct { X int } // not nullable, for backwards compatibility
PNestedNullable * struct { X int } ` bigquery:",nullable" `
}
type withTagsRepeated struct {
Repeated [ ] withTags ` bigquery:"repeated" `
RepeatedAnonymous [ ] struct {
ExcludeTag int ` bigquery:"-" `
Inside int ` bigquery:"inside" `
} ` bigquery:"anon" `
}
type withTagsEmbedded struct {
withTags
}
var withTagsSchema = Schema {
reqField ( "NoTag" , "INTEGER" ) ,
reqField ( "simple_tag" , "INTEGER" ) ,
reqField ( "_id" , "INTEGER" ) ,
reqField ( "MIXEDcase" , "INTEGER" ) ,
optField ( "Nullable" , "BYTES" ) ,
optField ( "NullNumeric" , "NUMERIC" ) ,
}
func TestTagInference ( t * testing . T ) {
testCases := [ ] struct {
in interface { }
want Schema
} {
{
in : withTags { } ,
want : withTagsSchema ,
} ,
{
in : withTagsNested { } ,
want : Schema {
& FieldSchema {
Name : "nested" ,
Required : true ,
Type : "RECORD" ,
Schema : withTagsSchema ,
} ,
& FieldSchema {
Name : "anon" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "inside" , "INTEGER" ) } ,
} ,
& FieldSchema {
Name : "PNested" ,
Required : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "X" , "INTEGER" ) } ,
} ,
& FieldSchema {
Name : "PNestedNullable" ,
Required : false ,
Type : "RECORD" ,
Schema : Schema { reqField ( "X" , "INTEGER" ) } ,
} ,
} ,
} ,
{
in : withTagsRepeated { } ,
want : Schema {
& FieldSchema {
Name : "repeated" ,
Repeated : true ,
Type : "RECORD" ,
Schema : withTagsSchema ,
} ,
& FieldSchema {
Name : "anon" ,
Repeated : true ,
Type : "RECORD" ,
Schema : Schema { reqField ( "inside" , "INTEGER" ) } ,
} ,
} ,
} ,
{
in : withTagsEmbedded { } ,
want : withTagsSchema ,
} ,
}
for i , tc := range testCases {
got , err := InferSchema ( tc . in )
if err != nil {
t . Fatalf ( "%d: error inferring TableSchema: %v" , i , err )
}
if ! testutil . Equal ( got , tc . want ) {
t . Errorf ( "%d: inferring TableSchema: got:\n%#v\nwant:\n%#v" , i ,
pretty . Value ( got ) , pretty . Value ( tc . want ) )
}
}
}
func TestTagInferenceErrors ( t * testing . T ) {
2019-03-17 20:19:56 +01:00
testCases := [ ] interface { } {
struct {
LongTag int ` bigquery:"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy" `
} { } ,
struct {
UnsupporedStartChar int ` bigquery:"øab" `
} { } ,
struct {
UnsupportedEndChar int ` bigquery:"abø" `
} { } ,
struct {
UnsupportedMiddleChar int ` bigquery:"aøb" `
} { } ,
struct {
StartInt int ` bigquery:"1abc" `
} { } ,
struct {
Hyphens int ` bigquery:"a-b" `
} { } ,
2018-06-19 15:30:26 +02:00
}
for i , tc := range testCases {
2019-03-17 20:19:56 +01:00
_ , got := InferSchema ( tc )
if _ , ok := got . ( invalidFieldNameError ) ; ! ok {
t . Errorf ( "%d: inferring TableSchema: got:\n%#v\nwant invalidFieldNameError" , i , got )
2018-06-19 15:30:26 +02:00
}
}
_ , err := InferSchema ( struct {
X int ` bigquery:",optional" `
} { } )
if err == nil {
t . Error ( "got nil, want error" )
}
}
func TestSchemaErrors ( t * testing . T ) {
testCases := [ ] struct {
2019-03-17 20:19:56 +01:00
in interface { }
want interface { }
2018-06-19 15:30:26 +02:00
} {
{
2019-03-17 20:19:56 +01:00
in : [ ] byte { } ,
want : noStructError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : new ( int ) ,
want : noStructError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Uint uint } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Uint64 uint64 } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Uintptr uintptr } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Complex complex64 } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Map map [ string ] int } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Chan chan bool } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Ptr * int } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { Interface interface { } } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { MultiDimensional [ ] [ ] int } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { MultiDimensional [ ] [ ] [ ] byte } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { SliceOfPointer [ ] * int } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { SliceOfNull [ ] NullInt64 } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { ChanSlice [ ] chan bool } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { NestedChan struct { Chan [ ] chan bool } } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
in : struct {
X int ` bigquery:",nullable" `
} { } ,
2019-03-17 20:19:56 +01:00
want : badNullableError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
in : struct {
X bool ` bigquery:",nullable" `
} { } ,
2019-03-17 20:19:56 +01:00
want : badNullableError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
in : struct {
X struct { N int } ` bigquery:",nullable" `
} { } ,
2019-03-17 20:19:56 +01:00
want : badNullableError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
in : struct {
X [ ] int ` bigquery:",nullable" `
} { } ,
2019-03-17 20:19:56 +01:00
want : badNullableError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { X * [ ] byte } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { X * [ ] int } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
{
2019-03-17 20:19:56 +01:00
in : struct { X * int } { } ,
want : unsupportedFieldTypeError { } ,
2018-06-19 15:30:26 +02:00
} ,
}
for _ , tc := range testCases {
_ , got := InferSchema ( tc . in )
2019-03-17 20:19:56 +01:00
if reflect . TypeOf ( got ) != reflect . TypeOf ( tc . want ) {
t . Errorf ( "%#v: got:\n%#v\nwant type %T" , tc . in , got , tc . want )
2018-06-19 15:30:26 +02:00
}
}
}
func TestHasRecursiveType ( t * testing . T ) {
type (
nonStruct int
nonRec struct { A string }
dup struct { A , B nonRec }
rec struct {
A int
B * rec
}
recUnexported struct {
A int
}
hasRec struct {
A int
R * rec
}
recSlicePointer struct {
A [ ] * recSlicePointer
}
)
for _ , test := range [ ] struct {
in interface { }
want bool
} {
{ nonStruct ( 0 ) , false } ,
{ nonRec { } , false } ,
{ dup { } , false } ,
{ rec { } , true } ,
{ recUnexported { } , false } ,
{ hasRec { } , true } ,
{ & recSlicePointer { } , true } ,
} {
got , err := hasRecursiveType ( reflect . TypeOf ( test . in ) , nil )
if err != nil {
t . Fatal ( err )
}
if got != test . want {
t . Errorf ( "%T: got %t, want %t" , test . in , got , test . want )
}
}
}
2019-03-17 20:19:56 +01:00
func TestSchemaFromJSON ( t * testing . T ) {
testCasesExpectingSuccess := [ ] struct {
bqSchemaJSON [ ] byte
description string
expectedSchema Schema
} {
{
description : "Flat table with a mixture of NULLABLE and REQUIRED fields" ,
bqSchemaJSON : [ ] byte ( `
[
{ "name" : "flat_string" , "type" : "STRING" , "mode" : "NULLABLE" , "description" : "Flat nullable string" } ,
{ "name" : "flat_bytes" , "type" : "BYTES" , "mode" : "REQUIRED" , "description" : "Flat required BYTES" } ,
{ "name" : "flat_integer" , "type" : "INTEGER" , "mode" : "NULLABLE" , "description" : "Flat nullable INTEGER" } ,
{ "name" : "flat_float" , "type" : "FLOAT" , "mode" : "REQUIRED" , "description" : "Flat required FLOAT" } ,
{ "name" : "flat_boolean" , "type" : "BOOLEAN" , "mode" : "NULLABLE" , "description" : "Flat nullable BOOLEAN" } ,
{ "name" : "flat_timestamp" , "type" : "TIMESTAMP" , "mode" : "REQUIRED" , "description" : "Flat required TIMESTAMP" } ,
{ "name" : "flat_date" , "type" : "DATE" , "mode" : "NULLABLE" , "description" : "Flat required DATE" } ,
{ "name" : "flat_time" , "type" : "TIME" , "mode" : "REQUIRED" , "description" : "Flat nullable TIME" } ,
{ "name" : "flat_datetime" , "type" : "DATETIME" , "mode" : "NULLABLE" , "description" : "Flat required DATETIME" } ,
{ "name" : "flat_numeric" , "type" : "NUMERIC" , "mode" : "REQUIRED" , "description" : "Flat nullable NUMERIC" } ,
{ "name" : "flat_geography" , "type" : "GEOGRAPHY" , "mode" : "REQUIRED" , "description" : "Flat required GEOGRAPHY" }
] ` ) ,
expectedSchema : Schema {
fieldSchema ( "Flat nullable string" , "flat_string" , "STRING" , false , false ) ,
fieldSchema ( "Flat required BYTES" , "flat_bytes" , "BYTES" , false , true ) ,
fieldSchema ( "Flat nullable INTEGER" , "flat_integer" , "INTEGER" , false , false ) ,
fieldSchema ( "Flat required FLOAT" , "flat_float" , "FLOAT" , false , true ) ,
fieldSchema ( "Flat nullable BOOLEAN" , "flat_boolean" , "BOOLEAN" , false , false ) ,
fieldSchema ( "Flat required TIMESTAMP" , "flat_timestamp" , "TIMESTAMP" , false , true ) ,
fieldSchema ( "Flat required DATE" , "flat_date" , "DATE" , false , false ) ,
fieldSchema ( "Flat nullable TIME" , "flat_time" , "TIME" , false , true ) ,
fieldSchema ( "Flat required DATETIME" , "flat_datetime" , "DATETIME" , false , false ) ,
fieldSchema ( "Flat nullable NUMERIC" , "flat_numeric" , "NUMERIC" , false , true ) ,
fieldSchema ( "Flat required GEOGRAPHY" , "flat_geography" , "GEOGRAPHY" , false , true ) ,
} ,
} ,
{
description : "Table with a nested RECORD" ,
bqSchemaJSON : [ ] byte ( `
[
{ "name" : "flat_string" , "type" : "STRING" , "mode" : "NULLABLE" , "description" : "Flat nullable string" } ,
{ "name" : "nested_record" , "type" : "RECORD" , "mode" : "NULLABLE" , "description" : "Nested nullable RECORD" , "fields" : [ { "name" : "record_field_1" , "type" : "STRING" , "mode" : "NULLABLE" , "description" : "First nested record field" } , { "name" : "record_field_2" , "type" : "INTEGER" , "mode" : "REQUIRED" , "description" : "Second nested record field" } ] }
] ` ) ,
expectedSchema : Schema {
fieldSchema ( "Flat nullable string" , "flat_string" , "STRING" , false , false ) ,
& FieldSchema {
Description : "Nested nullable RECORD" ,
Name : "nested_record" ,
Required : false ,
Type : "RECORD" ,
Schema : Schema {
{
Description : "First nested record field" ,
Name : "record_field_1" ,
Required : false ,
Type : "STRING" ,
} ,
{
Description : "Second nested record field" ,
Name : "record_field_2" ,
Required : true ,
Type : "INTEGER" ,
} ,
} ,
} ,
} ,
} ,
{
description : "Table with a repeated RECORD" ,
bqSchemaJSON : [ ] byte ( `
[
{ "name" : "flat_string" , "type" : "STRING" , "mode" : "NULLABLE" , "description" : "Flat nullable string" } ,
{ "name" : "nested_record" , "type" : "RECORD" , "mode" : "REPEATED" , "description" : "Nested nullable RECORD" , "fields" : [ { "name" : "record_field_1" , "type" : "STRING" , "mode" : "NULLABLE" , "description" : "First nested record field" } , { "name" : "record_field_2" , "type" : "INTEGER" , "mode" : "REQUIRED" , "description" : "Second nested record field" } ] }
] ` ) ,
expectedSchema : Schema {
fieldSchema ( "Flat nullable string" , "flat_string" , "STRING" , false , false ) ,
& FieldSchema {
Description : "Nested nullable RECORD" ,
Name : "nested_record" ,
Repeated : true ,
Required : false ,
Type : "RECORD" ,
Schema : Schema {
{
Description : "First nested record field" ,
Name : "record_field_1" ,
Required : false ,
Type : "STRING" ,
} ,
{
Description : "Second nested record field" ,
Name : "record_field_2" ,
Required : true ,
Type : "INTEGER" ,
} ,
} ,
} ,
} ,
} ,
}
for _ , tc := range testCasesExpectingSuccess {
convertedSchema , err := SchemaFromJSON ( tc . bqSchemaJSON )
if err != nil {
t . Errorf ( "encountered an error when converting JSON table schema (%s): %v" , tc . description , err )
continue
}
if ! testutil . Equal ( convertedSchema , tc . expectedSchema ) {
t . Errorf ( "generated JSON table schema (%s) differs from the expected schema" , tc . description )
}
}
testCasesExpectingFailure := [ ] struct {
bqSchemaJSON [ ] byte
description string
} {
{
description : "Schema with invalid JSON" ,
bqSchemaJSON : [ ] byte ( ` This is not JSON ` ) ,
} ,
{
description : "Schema with unknown field type" ,
bqSchemaJSON : [ ] byte ( ` [ { "name":"strange_type","type":"STRANGE","description":"This type should not exist"}] ` ) ,
} ,
{
description : "Schema with zero length" ,
bqSchemaJSON : [ ] byte ( ` ` ) ,
} ,
}
for _ , tc := range testCasesExpectingFailure {
_ , err := SchemaFromJSON ( tc . bqSchemaJSON )
if err == nil {
t . Errorf ( "converting this schema should have returned an error (%s): %v" , tc . description , err )
continue
}
}
}