Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bigquery): add support for allowing Javascript UDFs to indicate determinism #3534

Merged
23 changes: 19 additions & 4 deletions bigquery/integration_test.go
Expand Up @@ -2916,18 +2916,33 @@ func TestIntegration_RoutineJSUDF(t *testing.T) {
// Create a scalar UDF routine via API.
routineID := routineIDs.New()
routine := dataset.Routine(routineID)
err := routine.Create(ctx, &RoutineMetadata{
meta := &RoutineMetadata{
Language: "JAVASCRIPT", Type: "SCALAR_FUNCTION",
Description: "capitalizes using javascript",
Description: "capitalizes using javascript",
DeterminismLevel: Deterministic,
Arguments: []*RoutineArgument{
{Name: "instr", Kind: "FIXED_TYPE", DataType: &StandardSQLDataType{TypeKind: "STRING"}},
},
ReturnType: &StandardSQLDataType{TypeKind: "STRING"},
Body: "return instr.toUpperCase();",
})
if err != nil {
}
if err := routine.Create(ctx, meta); err != nil {
t.Fatalf("Create: %v", err)
}

newMeta := &RoutineMetadataToUpdate{
Language: meta.Language,
Body: meta.Body,
Arguments: meta.Arguments,
Description: meta.Description,
ReturnType: meta.ReturnType,
Type: meta.Type,

DeterminismLevel: NotDeterministic,
}
if _, err := routine.Update(ctx, newMeta, ""); err != nil {
t.Fatalf("Update: %v", err)
}
}

func TestIntegration_RoutineComplexTypes(t *testing.T) {
Expand Down
38 changes: 35 additions & 3 deletions bigquery/routine.go
Expand Up @@ -129,13 +129,27 @@ func (r *Routine) Delete(ctx context.Context) (err error) {
return req.Do()
}

// RoutineDeterminism specifies the level of determinism that javascript User Defined Functions
// exhibit.
type RoutineDeterminism string

const (
// Deterministic indicates that two calls with the same input to a UDF yield the same output.
Deterministic RoutineDeterminism = "DETERMINISTIC"
// NotDeterministic indicates that the output of the UDF is not guaranteed to yield the same
// output each time for a given set of inputs.
NotDeterministic RoutineDeterminism = "NOT_DETERMINISTIC"
)

// RoutineMetadata represents details of a given BigQuery Routine.
type RoutineMetadata struct {
ETag string
// Type indicates the type of routine, such as SCALAR_FUNCTION or PROCEDURE.
Type string
CreationTime time.Time
Description string
Type string
CreationTime time.Time
Description string
// DeterminismLevel is only applicable to Javascript UDFs.
DeterminismLevel RoutineDeterminism
LastModifiedTime time.Time
// Language of the routine, such as SQL or JAVASCRIPT.
Language string
Expand All @@ -161,6 +175,7 @@ func (rm *RoutineMetadata) toBQ() (*bq.Routine, error) {
return r, nil
}
r.Description = rm.Description
r.DeterminismLevel = string(rm.DeterminismLevel)
r.Language = rm.Language
r.RoutineType = rm.Type
r.DefinitionBody = rm.Body
Expand Down Expand Up @@ -280,6 +295,7 @@ func routineArgumentsToBQ(in []*RoutineArgument) ([]*bq.Argument, error) {
type RoutineMetadataToUpdate struct {
Arguments []*RoutineArgument
Description optional.String
DeterminismLevel optional.String
Type optional.String
Language optional.String
Body optional.String
Expand All @@ -299,6 +315,21 @@ func (rm *RoutineMetadataToUpdate) toBQ() (*bq.Routine, error) {
r.Description = optional.ToString(rm.Description)
forceSend("Description")
}
if rm.DeterminismLevel != nil {
processed := false
// Allow either string or RoutineDeterminism, a type based on string.
if x, ok := rm.DeterminismLevel.(RoutineDeterminism); ok {
r.DeterminismLevel = string(x)
processed = true
}
if x, ok := rm.DeterminismLevel.(string); ok {
r.DeterminismLevel = x
processed = true
}
if !processed {
panic(fmt.Sprintf("DeterminismLevel should be either type string or RoutineDetermism in update, got %T", rm.DeterminismLevel))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why panic instead of returning error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mimics the behavior of the converters in https://pkg.go.dev/cloud.google.com/go/internal/optional. All the other fields panic if you use incorrect type, so yielding a custom error for the one field seems a mismatch.

}
}
if rm.Arguments != nil {
if len(rm.Arguments) == 0 {
nullField("Arguments")
Expand Down Expand Up @@ -348,6 +379,7 @@ func bqToRoutineMetadata(r *bq.Routine) (*RoutineMetadata, error) {
Type: r.RoutineType,
CreationTime: unixMillisToTime(r.CreationTime),
Description: r.Description,
DeterminismLevel: RoutineDeterminism(r.DeterminismLevel),
LastModifiedTime: unixMillisToTime(r.LastModifiedTime),
Language: r.Language,
ImportedLibraries: r.ImportedLibraries,
Expand Down
2 changes: 2 additions & 0 deletions bigquery/routine_test.go
Expand Up @@ -80,6 +80,7 @@ func TestRoutineTypeConversions(t *testing.T) {
DefinitionBody: "body",
Description: "desc",
Etag: "etag",
DeterminismLevel: "DETERMINISTIC",
RoutineType: "type",
Language: "lang",
ReturnType: &bq.StandardSqlDataType{TypeKind: "INT64"},
Expand All @@ -88,6 +89,7 @@ func TestRoutineTypeConversions(t *testing.T) {
CreationTime: aTime,
LastModifiedTime: aTime,
Description: "desc",
DeterminismLevel: Deterministic,
Body: "body",
ETag: "etag",
Type: "type",
Expand Down