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