1// Copyright 2019 Google LLC 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package bigquery 16 17import ( 18 "context" 19 "errors" 20 "fmt" 21 "time" 22 23 "cloud.google.com/go/internal/optional" 24 "cloud.google.com/go/internal/trace" 25 bq "google.golang.org/api/bigquery/v2" 26) 27 28// Routine represents a reference to a BigQuery routine. There are multiple 29// types of routines including stored procedures and scalar user-defined functions (UDFs). 30// For more information, see the BigQuery documentation at https://cloud.google.com/bigquery/docs/ 31type Routine struct { 32 ProjectID string 33 DatasetID string 34 RoutineID string 35 36 c *Client 37} 38 39// FullyQualifiedName returns an identifer for the routine in project.dataset.routine format. 40func (r *Routine) FullyQualifiedName() string { 41 return fmt.Sprintf("%s.%s.%s", r.ProjectID, r.DatasetID, r.RoutineID) 42} 43 44// Create creates a Routine in the BigQuery service. 45// Pass in a RoutineMetadata to define the routine. 46func (r *Routine) Create(ctx context.Context, rm *RoutineMetadata) (err error) { 47 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Routine.Create") 48 defer func() { trace.EndSpan(ctx, err) }() 49 50 routine, err := rm.toBQ() 51 if err != nil { 52 return err 53 } 54 routine.RoutineReference = &bq.RoutineReference{ 55 ProjectId: r.ProjectID, 56 DatasetId: r.DatasetID, 57 RoutineId: r.RoutineID, 58 } 59 req := r.c.bqs.Routines.Insert(r.ProjectID, r.DatasetID, routine).Context(ctx) 60 setClientHeader(req.Header()) 61 _, err = req.Do() 62 return err 63} 64 65// Metadata fetches the metadata for a given Routine. 66func (r *Routine) Metadata(ctx context.Context) (rm *RoutineMetadata, err error) { 67 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Routine.Metadata") 68 defer func() { trace.EndSpan(ctx, err) }() 69 70 req := r.c.bqs.Routines.Get(r.ProjectID, r.DatasetID, r.RoutineID).Context(ctx) 71 setClientHeader(req.Header()) 72 var routine *bq.Routine 73 err = runWithRetry(ctx, func() (err error) { 74 routine, err = req.Do() 75 return err 76 }) 77 if err != nil { 78 return nil, err 79 } 80 return bqToRoutineMetadata(routine) 81} 82 83// Update modifies properties of a Routine using the API. 84func (r *Routine) Update(ctx context.Context, upd *RoutineMetadataToUpdate, etag string) (rm *RoutineMetadata, err error) { 85 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Routine.Update") 86 defer func() { trace.EndSpan(ctx, err) }() 87 88 bqr, err := upd.toBQ() 89 if err != nil { 90 return nil, err 91 } 92 //TODO: remove when routines update supports partial requests. 93 bqr.RoutineReference = &bq.RoutineReference{ 94 ProjectId: r.ProjectID, 95 DatasetId: r.DatasetID, 96 RoutineId: r.RoutineID, 97 } 98 99 call := r.c.bqs.Routines.Update(r.ProjectID, r.DatasetID, r.RoutineID, bqr).Context(ctx) 100 setClientHeader(call.Header()) 101 if etag != "" { 102 call.Header().Set("If-Match", etag) 103 } 104 var res *bq.Routine 105 if err := runWithRetry(ctx, func() (err error) { 106 res, err = call.Do() 107 return err 108 }); err != nil { 109 return nil, err 110 } 111 return bqToRoutineMetadata(res) 112} 113 114// Delete removes a Routine from a dataset. 115func (r *Routine) Delete(ctx context.Context) (err error) { 116 ctx = trace.StartSpan(ctx, "cloud.google.com/go/bigquery.Model.Delete") 117 defer func() { trace.EndSpan(ctx, err) }() 118 119 req := r.c.bqs.Routines.Delete(r.ProjectID, r.DatasetID, r.RoutineID).Context(ctx) 120 setClientHeader(req.Header()) 121 return req.Do() 122} 123 124// RoutineMetadata represents details of a given BigQuery Routine. 125type RoutineMetadata struct { 126 ETag string 127 // Type indicates the type of routine, such as SCALAR_FUNCTION or PROCEDURE. 128 Type string 129 CreationTime time.Time 130 Description string 131 LastModifiedTime time.Time 132 // Language of the routine, such as SQL or JAVASCRIPT. 133 Language string 134 // The list of arguments for the the routine. 135 Arguments []*RoutineArgument 136 ReturnType *StandardSQLDataType 137 // For javascript routines, this indicates the paths for imported libraries. 138 ImportedLibraries []string 139 // Body contains the routine's body. 140 // For functions, Body is the expression in the AS clause. 141 // 142 // For SQL functions, it is the substring inside the parentheses of a CREATE 143 // FUNCTION statement. 144 // 145 // For JAVASCRIPT function, it is the evaluated string in the AS clause of 146 // a CREATE FUNCTION statement. 147 Body string 148} 149 150func (rm *RoutineMetadata) toBQ() (*bq.Routine, error) { 151 r := &bq.Routine{} 152 if rm == nil { 153 return r, nil 154 } 155 r.Description = rm.Description 156 r.Language = rm.Language 157 r.RoutineType = rm.Type 158 r.DefinitionBody = rm.Body 159 160 var args []*bq.Argument 161 for _, v := range rm.Arguments { 162 bqa, err := v.toBQ() 163 if err != nil { 164 return nil, err 165 } 166 args = append(args, bqa) 167 } 168 r.Arguments = args 169 r.ImportedLibraries = rm.ImportedLibraries 170 if !rm.CreationTime.IsZero() { 171 return nil, errors.New("cannot set CreationTime on create") 172 } 173 if !rm.LastModifiedTime.IsZero() { 174 return nil, errors.New("cannot set LastModifiedTime on create") 175 } 176 if rm.ETag != "" { 177 return nil, errors.New("cannot set ETag on create") 178 } 179 return r, nil 180} 181 182// RoutineArgument represents an argument supplied to a routine such as a UDF or 183// stored procedured. 184type RoutineArgument struct { 185 // The name of this argument. Can be absent for function return argument. 186 Name string 187 // Kind indicates the kind of argument represented. 188 // Possible values: 189 // ARGUMENT_KIND_UNSPECIFIED 190 // FIXED_TYPE - The argument is a variable with fully specified 191 // type, which can be a struct or an array, but not a table. 192 // ANY_TYPE - The argument is any type, including struct or array, 193 // but not a table. 194 Kind string 195 // Mode is optional, and indicates whether an argument is input or output. 196 // Mode can only be set for procedures. 197 // 198 // Possible values: 199 // MODE_UNSPECIFIED 200 // IN - The argument is input-only. 201 // OUT - The argument is output-only. 202 // INOUT - The argument is both an input and an output. 203 Mode string 204 // DataType provides typing information. Unnecessary for ANY_TYPE Kind 205 // arguments. 206 DataType *StandardSQLDataType 207} 208 209func (ra *RoutineArgument) toBQ() (*bq.Argument, error) { 210 if ra == nil { 211 return nil, nil 212 } 213 a := &bq.Argument{ 214 Name: ra.Name, 215 ArgumentKind: ra.Kind, 216 Mode: ra.Mode, 217 } 218 if ra.DataType != nil { 219 dt, err := ra.DataType.toBQ() 220 if err != nil { 221 return nil, err 222 } 223 a.DataType = dt 224 } 225 return a, nil 226} 227 228func bqToRoutineArgument(bqa *bq.Argument) (*RoutineArgument, error) { 229 arg := &RoutineArgument{ 230 Name: bqa.Name, 231 Kind: bqa.ArgumentKind, 232 Mode: bqa.Mode, 233 } 234 dt, err := bqToStandardSQLDataType(bqa.DataType) 235 if err != nil { 236 return nil, err 237 } 238 arg.DataType = dt 239 return arg, nil 240} 241 242func bqToArgs(in []*bq.Argument) ([]*RoutineArgument, error) { 243 var out []*RoutineArgument 244 for _, a := range in { 245 arg, err := bqToRoutineArgument(a) 246 if err != nil { 247 return nil, err 248 } 249 out = append(out, arg) 250 } 251 return out, nil 252} 253 254func routineArgumentsToBQ(in []*RoutineArgument) ([]*bq.Argument, error) { 255 var out []*bq.Argument 256 for _, inarg := range in { 257 arg, err := inarg.toBQ() 258 if err != nil { 259 return nil, err 260 } 261 out = append(out, arg) 262 } 263 return out, nil 264} 265 266// RoutineMetadataToUpdate governs updating a routine. 267type RoutineMetadataToUpdate struct { 268 Arguments []*RoutineArgument 269 Description optional.String 270 Type optional.String 271 Language optional.String 272 Body optional.String 273 ImportedLibraries []string 274 ReturnType *StandardSQLDataType 275} 276 277func (rm *RoutineMetadataToUpdate) toBQ() (*bq.Routine, error) { 278 r := &bq.Routine{} 279 forceSend := func(field string) { 280 r.ForceSendFields = append(r.ForceSendFields, field) 281 } 282 nullField := func(field string) { 283 r.NullFields = append(r.NullFields, field) 284 } 285 if rm.Description != nil { 286 r.Description = optional.ToString(rm.Description) 287 forceSend("Description") 288 } 289 if rm.Arguments != nil { 290 if len(rm.Arguments) == 0 { 291 nullField("Arguments") 292 } else { 293 args, err := routineArgumentsToBQ(rm.Arguments) 294 if err != nil { 295 return nil, err 296 } 297 r.Arguments = args 298 forceSend("Arguments") 299 } 300 } 301 if rm.Type != nil { 302 r.RoutineType = optional.ToString(rm.Type) 303 forceSend("RoutineType") 304 } 305 if rm.Language != nil { 306 r.Language = optional.ToString(rm.Language) 307 forceSend("Language") 308 } 309 if rm.Body != nil { 310 r.DefinitionBody = optional.ToString(rm.Body) 311 forceSend("DefinitionBody") 312 } 313 if rm.ImportedLibraries != nil { 314 if len(rm.ImportedLibraries) == 0 { 315 nullField("ImportedLibraries") 316 } else { 317 r.ImportedLibraries = rm.ImportedLibraries 318 forceSend("ImportedLibraries") 319 } 320 } 321 if rm.ReturnType != nil { 322 dt, err := rm.ReturnType.toBQ() 323 if err != nil { 324 return nil, err 325 } 326 r.ReturnType = dt 327 forceSend("ReturnType") 328 } 329 return r, nil 330} 331 332func bqToRoutineMetadata(r *bq.Routine) (*RoutineMetadata, error) { 333 meta := &RoutineMetadata{ 334 ETag: r.Etag, 335 Type: r.RoutineType, 336 CreationTime: unixMillisToTime(r.CreationTime), 337 Description: r.Description, 338 LastModifiedTime: unixMillisToTime(r.LastModifiedTime), 339 Language: r.Language, 340 ImportedLibraries: r.ImportedLibraries, 341 Body: r.DefinitionBody, 342 } 343 args, err := bqToArgs(r.Arguments) 344 if err != nil { 345 return nil, err 346 } 347 meta.Arguments = args 348 ret, err := bqToStandardSQLDataType(r.ReturnType) 349 if err != nil { 350 return nil, err 351 } 352 meta.ReturnType = ret 353 return meta, nil 354} 355