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