Skip to content

Commit

Permalink
feat(bigquery): add schema support for RANGE type (#9050)
Browse files Browse the repository at this point in the history
This PR adds the basic schema plumbing support needed to define a schema with a RANGE type.

Towards: https://github.com/googleapis/google-cloud-go/issues/9017
  • Loading branch information
shollyman authored Dec 5, 2023
1 parent c488c51 commit 477ccee
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 1 deletion.
35 changes: 35 additions & 0 deletions bigquery/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ type FieldSchema struct {
// - '': empty string. Default to case-sensitive behavior.
// More information: https://cloud.google.com/bigquery/docs/reference/standard-sql/collation-concepts
Collation string

// Information about the range.
// If the type is RANGE, this field is required.
RangeElementType *RangeElementType
}

func (fs *FieldSchema) toBQ() *bq.TableFieldSchema {
Expand All @@ -161,6 +165,7 @@ func (fs *FieldSchema) toBQ() *bq.TableFieldSchema {
Scale: fs.Scale,
DefaultValueExpression: fs.DefaultValueExpression,
Collation: string(fs.Collation),
RangeElementType: fs.RangeElementType.toBQ(),
}

if fs.Repeated {
Expand All @@ -176,6 +181,32 @@ func (fs *FieldSchema) toBQ() *bq.TableFieldSchema {
return tfs
}

// RangeElementType describes information about the range type.
type RangeElementType struct {
// The subtype of the RANGE, if the type of this field is RANGE.
// Possible values for the field element type of a RANGE include:
// DATE, DATETIME, or TIMESTAMP.
Type FieldType
}

func (rt *RangeElementType) toBQ() *bq.TableFieldSchemaRangeElementType {
if rt == nil {
return nil
}
return &bq.TableFieldSchemaRangeElementType{
Type: string(rt.Type),
}
}

func bqToRangeElementType(rt *bq.TableFieldSchemaRangeElementType) *RangeElementType {
if rt == nil {
return nil
}
return &RangeElementType{
Type: FieldType(rt.Type),
}
}

// PolicyTagList represents the annotations on a schema column for enforcing column-level security.
// For more information, see https://cloud.google.com/bigquery/docs/column-level-security-intro
type PolicyTagList struct {
Expand Down Expand Up @@ -221,6 +252,7 @@ func bqToFieldSchema(tfs *bq.TableFieldSchema) *FieldSchema {
Scale: tfs.Scale,
DefaultValueExpression: tfs.DefaultValueExpression,
Collation: tfs.Collation,
RangeElementType: bqToRangeElementType(tfs.RangeElementType),
}

for _, f := range tfs.Fields {
Expand Down Expand Up @@ -277,6 +309,8 @@ const (
IntervalFieldType FieldType = "INTERVAL"
// JSONFieldType is a representation of a json object.
JSONFieldType FieldType = "JSON"
// RangeFieldType represents a continuous range of values.
RangeFieldType FieldType = "RANGE"
)

var (
Expand All @@ -297,6 +331,7 @@ var (
BigNumericFieldType: true,
IntervalFieldType: true,
JSONFieldType: true,
RangeFieldType: true,
}
// The API will accept alias names for the types based on the Standard SQL type names.
fieldAliases = map[FieldType]FieldType{
Expand Down
23 changes: 23 additions & 0 deletions bigquery/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,29 @@ func TestSchemaConversion(t *testing.T) {
},
},
},
{
// RANGE
bqSchema: &bq.TableSchema{
Fields: []*bq.TableFieldSchema{
func() *bq.TableFieldSchema {
f := bqTableFieldSchema("desc", "rt", "RANGE", "", nil)
f.RangeElementType = &bq.TableFieldSchemaRangeElementType{
Type: "DATE",
}
return f
}(),
},
},
schema: Schema{
func() *FieldSchema {
f := fieldSchema("desc", "rt", "RANGE", false, false, nil)
f.RangeElementType = &RangeElementType{
Type: DateFieldType,
}
return f
}(),
},
},
}
for _, tc := range testCases {
bqSchema := tc.schema.toBQ()
Expand Down
36 changes: 35 additions & 1 deletion bigquery/table_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
"google.golang.org/api/iterator"
)

func TestIntegration_TableCreate(t *testing.T) {
func TestIntegration_TableInvalidSchema(t *testing.T) {
// Check that creating a record field with an empty schema is an error.
if client == nil {
t.Skip("Integration tests skipped")
Expand All @@ -51,6 +51,40 @@ func TestIntegration_TableCreate(t *testing.T) {
}
}

func TestIntegration_TableValidSchema(t *testing.T) {
if client == nil {
t.Skip("Integration tests skipped")
}
ctx := context.Background()
table := dataset.Table("t_bad")
schema := Schema{
{
Name: "range_dt",
Type: RangeFieldType,
RangeElementType: &RangeElementType{
Type: DateTimeFieldType,
},
},
{Name: "rec", Type: RecordFieldType, Schema: Schema{
{Name: "inner", Type: IntegerFieldType},
}},
}
err := table.Create(ctx, &TableMetadata{
Schema: schema,
})
if err != nil {
t.Fatalf("table.Create: %v", err)
}

meta, err := table.Metadata(ctx)
if err != nil {
t.Fatalf("table.Metadata: %v", err)
}
if diff := testutil.Diff(meta.Schema, schema); diff != "" {
t.Fatalf("got=-, want=+:\n%s", diff)
}
}

func TestIntegration_TableCreateWithConstraints(t *testing.T) {
if client == nil {
t.Skip("Integration tests skipped")
Expand Down

0 comments on commit 477ccee

Please sign in to comment.