diff --git a/pkg/resources/external_function.go b/pkg/resources/external_function.go index 5bdff6e3b3..a78e9c5d6b 100644 --- a/pkg/resources/external_function.go +++ b/pkg/resources/external_function.go @@ -1,506 +1,489 @@ package resources -import ( - "context" - "encoding/json" - "log" - "regexp" - "strconv" - "strings" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -var externalFunctionSchema = map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Specifies the identifier for the external function. The identifier can contain the schema name and database name, as well as the function name. The function's signature (name and argument data types) must be unique within the schema.", - }, - "schema": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The schema in which to create the external function.", - }, - "database": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The database in which to create the external function.", - }, - "arg": { - Type: schema.TypeList, - Optional: true, - ForceNew: true, - Description: "Specifies the arguments/inputs for the external function. These should correspond to the arguments that the remote service expects.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - // Suppress the diff shown if the values are equal when both compared in lower case. - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) - }, - Description: "Argument name", - }, - "type": { - Type: schema.TypeString, - Required: true, - // Suppress the diff shown if the values are equal when both compared in lower case. - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) - }, - Description: "Argument type, e.g. VARCHAR", - }, - }, - }, - }, - "null_input_behavior": { - Type: schema.TypeString, - Optional: true, - Default: "CALLED ON NULL INPUT", - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{"CALLED ON NULL INPUT", "RETURNS NULL ON NULL INPUT", "STRICT"}, false), - Description: "Specifies the behavior of the external function when called with null inputs.", - }, - "return_type": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - // Suppress the diff shown if the values are equal when both compared in lower case. - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) - }, - Description: "Specifies the data type returned by the external function.", - }, - "return_null_allowed": { - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - Description: "Indicates whether the function can return NULL values (true) or must return only NON-NULL values (false).", - Default: true, - }, - "return_behavior": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{"VOLATILE", "IMMUTABLE"}, false), - Description: "Specifies the behavior of the function when returning results", - }, - "api_integration": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "The name of the API integration object that should be used to authenticate the call to the proxy service.", - }, - "header": { - Type: schema.TypeSet, - Optional: true, - ForceNew: true, - Description: "Allows users to specify key-value metadata that is sent with every request as HTTP headers.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Header name", - }, - "value": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Header value", - }, - }, - }, - }, - "context_headers": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - ForceNew: true, - // Suppress the diff shown if the values are equal when both compared in lower case. - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) - }, - Description: "Binds Snowflake context function results to HTTP headers.", - }, - "max_batch_rows": { - Type: schema.TypeInt, - Optional: true, - ForceNew: true, - Description: "This specifies the maximum number of rows in each batch sent to the proxy service.", - }, - "compression": { - Type: schema.TypeString, - Optional: true, - Default: "AUTO", - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{"NONE", "AUTO", "GZIP", "DEFLATE"}, false), - Description: "If specified, the JSON payload is compressed when sent from Snowflake to the proxy service, and when sent back from the proxy service to Snowflake.", - }, - "request_translator": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "This specifies the name of the request translator function", - }, - "response_translator": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "This specifies the name of the response translator function.", - }, - "url_of_proxy_and_resource": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "This is the invocation URL of the proxy service and resource through which Snowflake calls the remote service.", - }, - "comment": { - Type: schema.TypeString, - Optional: true, - Default: "user-defined function", - Description: "A description of the external function.", - }, - "created_on": { - Type: schema.TypeString, - Computed: true, - Description: "Date and time when the external function was created.", - }, -} - -// ExternalFunction returns a pointer to the resource representing an external function. -func ExternalFunction() *schema.Resource { - return &schema.Resource{ - SchemaVersion: 1, - - CreateContext: CreateContextExternalFunction, - ReadContext: ReadContextExternalFunction, - UpdateContext: UpdateContextExternalFunction, - DeleteContext: DeleteContextExternalFunction, - - Schema: externalFunctionSchema, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - StateUpgraders: []schema.StateUpgrader{ - { - Version: 0, - // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject - Type: cty.EmptyObject, - Upgrade: v085ExternalFunctionStateUpgrader, - }, - }, - } -} - -func CreateContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*provider.Context).Client - database := d.Get("database").(string) - schemaName := d.Get("schema").(string) - name := d.Get("name").(string) - id := sdk.NewSchemaObjectIdentifier(database, schemaName, name) - - returnType := d.Get("return_type").(string) - resultDataType, err := sdk.ToDataType(returnType) - if err != nil { - return diag.FromErr(err) - } - apiIntegration := sdk.NewAccountObjectIdentifier(d.Get("api_integration").(string)) - urlOfProxyAndResource := d.Get("url_of_proxy_and_resource").(string) - req := sdk.NewCreateExternalFunctionRequest(id, resultDataType, &apiIntegration, urlOfProxyAndResource) - - // Set optionals - args := make([]sdk.ExternalFunctionArgumentRequest, 0) - if v, ok := d.GetOk("arg"); ok { - for _, arg := range v.([]interface{}) { - argName := arg.(map[string]interface{})["name"].(string) - argType := arg.(map[string]interface{})["type"].(string) - argDataType, err := sdk.ToDataType(argType) - if err != nil { - return diag.FromErr(err) - } - args = append(args, sdk.ExternalFunctionArgumentRequest{ArgName: argName, ArgDataType: argDataType}) - } - } - if len(args) > 0 { - req.WithArguments(args) - } - - if v, ok := d.GetOk("return_null_allowed"); ok { - if v.(bool) { - req.WithReturnNullValues(&sdk.ReturnNullValuesNull) - } else { - req.WithReturnNullValues(&sdk.ReturnNullValuesNotNull) - } - } - - if v, ok := d.GetOk("return_behavior"); ok { - if v.(string) == "VOLATILE" { - req.WithReturnResultsBehavior(&sdk.ReturnResultsBehaviorVolatile) - } else { - req.WithReturnResultsBehavior(&sdk.ReturnResultsBehaviorImmutable) - } - } - - if v, ok := d.GetOk("null_input_behavior"); ok { - switch { - case v.(string) == "CALLED ON NULL INPUT": - req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehaviorCalledOnNullInput)) - case v.(string) == "RETURNS NULL ON NULL INPUT": - req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehaviorReturnNullInput)) - default: - req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehaviorStrict)) - } - } - - if v, ok := d.GetOk("comment"); ok { - req.WithComment(sdk.String(v.(string))) - } - - if _, ok := d.GetOk("header"); ok { - headers := make([]sdk.ExternalFunctionHeaderRequest, 0) - for _, header := range d.Get("header").(*schema.Set).List() { - m := header.(map[string]interface{}) - headerName := m["name"].(string) - headerValue := m["value"].(string) - headers = append(headers, sdk.ExternalFunctionHeaderRequest{ - Name: headerName, - Value: headerValue, - }) - } - req.WithHeaders(headers) - } - - if v, ok := d.GetOk("context_headers"); ok { - contextHeadersList := expandStringList(v.([]interface{})) - contextHeaders := make([]sdk.ExternalFunctionContextHeaderRequest, 0) - for _, header := range contextHeadersList { - contextHeaders = append(contextHeaders, sdk.ExternalFunctionContextHeaderRequest{ - ContextFunction: header, - }) - } - req.WithContextHeaders(contextHeaders) - } - - if v, ok := d.GetOk("max_batch_rows"); ok { - req.WithMaxBatchRows(sdk.Int(v.(int))) - } - - if v, ok := d.GetOk("compression"); ok { - req.WithCompression(sdk.String(v.(string))) - } - - if v, ok := d.GetOk("request_translator"); ok { - req.WithRequestTranslator(sdk.Pointer(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(v.(string)))) - } - - if v, ok := d.GetOk("response_translator"); ok { - req.WithResponseTranslator(sdk.Pointer(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(v.(string)))) - } - - if err := client.ExternalFunctions.Create(ctx, req); err != nil { - return diag.FromErr(err) - } - argTypes := make([]sdk.DataType, 0, len(args)) - for _, item := range args { - argTypes = append(argTypes, item.ArgDataType) - } - sid := sdk.NewSchemaObjectIdentifierWithArguments(database, schemaName, name, argTypes) - d.SetId(sid.FullyQualifiedName()) - return ReadContextExternalFunction(ctx, d, meta) -} - -func ReadContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*provider.Context).Client - - id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) - externalFunction, err := client.ExternalFunctions.ShowByID(ctx, id) - if err != nil { - d.SetId("") - return nil - } - - // Some properties can come from the SHOW EXTERNAL FUNCTION call - if err := d.Set("name", externalFunction.Name); err != nil { - return diag.FromErr(err) - } - - if err := d.Set("schema", strings.Trim(externalFunction.SchemaName, "\"")); err != nil { - return diag.FromErr(err) - } - - if err := d.Set("database", strings.Trim(externalFunction.CatalogName, "\"")); err != nil { - return diag.FromErr(err) - } - - if err := d.Set("comment", externalFunction.Description); err != nil { - return diag.FromErr(err) - } - - if err := d.Set("created_on", externalFunction.CreatedOn); err != nil { - return diag.FromErr(err) - } - - // Some properties come from the DESCRIBE FUNCTION call - externalFunctionPropertyRows, err := client.ExternalFunctions.Describe(ctx, sdk.NewDescribeExternalFunctionRequest(id.WithoutArguments(), id.Arguments())) - if err != nil { - d.SetId("") - return nil - } - - for _, row := range externalFunctionPropertyRows { - switch row.Property { - case "signature": - // Format in Snowflake DB is: (argName argType, argName argType, ...) - args := strings.ReplaceAll(strings.ReplaceAll(row.Value, "(", ""), ")", "") - - if args != "" { // Do nothing for functions without arguments - argPairs := strings.Split(args, ", ") - args := []interface{}{} - - for _, argPair := range argPairs { - argItem := strings.Split(argPair, " ") - - arg := map[string]interface{}{} - arg["name"] = argItem[0] - arg["type"] = argItem[1] - args = append(args, arg) - } - - if err := d.Set("arg", args); err != nil { - return diag.Errorf("error setting arg: %v", err) - } - } - case "returns": - returnType := row.Value - // We first check for VARIANT or OBJECT - if returnType == "VARIANT" || returnType == "OBJECT" { - if err := d.Set("return_type", returnType); err != nil { - return diag.Errorf("error setting return_type: %v", err) - } - break - } - - // otherwise, format in Snowflake DB is returnType() - re := regexp.MustCompile(`^(\w+)\([0-9]*\)$`) - match := re.FindStringSubmatch(row.Value) - if len(match) < 2 { - return diag.Errorf("return_type %s not recognized", returnType) - } - if err := d.Set("return_type", match[1]); err != nil { - return diag.Errorf("error setting return_type: %v", err) - } - - case "null handling": - if err := d.Set("null_input_behavior", row.Value); err != nil { - return diag.Errorf("error setting null_input_behavior: %v", err) - } - case "volatility": - if err := d.Set("return_behavior", row.Value); err != nil { - return diag.Errorf("error setting return_behavior: %v", err) - } - case "headers": - if row.Value != "" && row.Value != "null" { - // Format in Snowflake DB is: {"head1":"val1","head2":"val2"} - var jsonHeaders map[string]string - err := json.Unmarshal([]byte(row.Value), &jsonHeaders) - if err != nil { - return diag.Errorf("error unmarshalling headers: %v", err) - } - - headers := make([]any, 0, len(jsonHeaders)) - for key, value := range jsonHeaders { - headers = append(headers, map[string]any{ - "name": key, - "value": value, - }) - } - - if err := d.Set("header", headers); err != nil { - return diag.Errorf("error setting return_behavior: %v", err) - } - } - case "context_headers": - if row.Value != "" && row.Value != "null" { - // Format in Snowflake DB is: ["CONTEXT_FUNCTION_1","CONTEXT_FUNCTION_2"] - contextHeaders := strings.Split(strings.Trim(row.Value, "[]"), ",") - for i, v := range contextHeaders { - contextHeaders[i] = strings.Trim(v, "\"") - } - if err := d.Set("context_headers", contextHeaders); err != nil { - return diag.Errorf("error setting context_headers: %v", err) - } - } - case "max_batch_rows": - if row.Value != "not set" { - maxBatchRows, err := strconv.ParseInt(row.Value, 10, 64) - if err != nil { - return diag.Errorf("error parsing max_batch_rows: %v", err) - } - if err := d.Set("max_batch_rows", maxBatchRows); err != nil { - return diag.Errorf("error setting max_batch_rows: %v", err) - } - } - case "compression": - if err := d.Set("compression", row.Value); err != nil { - return diag.Errorf("error setting compression: %v", err) - } - case "body": - if err := d.Set("url_of_proxy_and_resource", row.Value); err != nil { - return diag.Errorf("error setting url_of_proxy_and_resource: %v", err) - } - case "language": - // To ignore - default: - log.Printf("[WARN] unexpected external function property %v returned from Snowflake", row.Property) - } - } - - return nil -} - -func UpdateContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*provider.Context).Client - - id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) - req := sdk.NewAlterFunctionRequest(id.WithoutArguments(), id.Arguments()) - if d.HasChange("comment") { - _, new := d.GetChange("comment") - if new == "" { - req.UnsetComment = sdk.Bool(true) - } else { - req.SetComment = sdk.String(new.(string)) - } - err := client.Functions.Alter(ctx, req) - if err != nil { - return diag.FromErr(err) - } - } - return ReadContextExternalFunction(ctx, d, meta) -} - -func DeleteContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*provider.Context).Client - - id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) - req := sdk.NewDropFunctionRequest(id.WithoutArguments(), id.Arguments()) - if err := client.Functions.Drop(ctx, req); err != nil { - return diag.FromErr(err) - } - - d.SetId("") - return nil -} +//var externalFunctionSchema = map[string]*schema.Schema{ +// "name": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "Specifies the identifier for the external function. The identifier can contain the schema name and database name, as well as the function name. The function's signature (name and argument data types) must be unique within the schema.", +// }, +// "schema": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "The schema in which to create the external function.", +// }, +// "database": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "The database in which to create the external function.", +// }, +// "arg": { +// Type: schema.TypeList, +// Optional: true, +// ForceNew: true, +// Description: "Specifies the arguments/inputs for the external function. These should correspond to the arguments that the remote service expects.", +// Elem: &schema.Resource{ +// Schema: map[string]*schema.Schema{ +// "name": { +// Type: schema.TypeString, +// Required: true, +// // Suppress the diff shown if the values are equal when both compared in lower case. +// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { +// return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) +// }, +// Description: "Argument name", +// }, +// "type": { +// Type: schema.TypeString, +// Required: true, +// // Suppress the diff shown if the values are equal when both compared in lower case. +// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { +// return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) +// }, +// Description: "Argument type, e.g. VARCHAR", +// }, +// }, +// }, +// }, +// "null_input_behavior": { +// Type: schema.TypeString, +// Optional: true, +// Default: "CALLED ON NULL INPUT", +// ForceNew: true, +// ValidateFunc: validation.StringInSlice([]string{"CALLED ON NULL INPUT", "RETURNS NULL ON NULL INPUT", "STRICT"}, false), +// Description: "Specifies the behavior of the external function when called with null inputs.", +// }, +// "return_type": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// // Suppress the diff shown if the values are equal when both compared in lower case. +// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { +// return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) +// }, +// Description: "Specifies the data type returned by the external function.", +// }, +// "return_null_allowed": { +// Type: schema.TypeBool, +// Optional: true, +// ForceNew: true, +// Description: "Indicates whether the function can return NULL values (true) or must return only NON-NULL values (false).", +// Default: true, +// }, +// "return_behavior": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// ValidateFunc: validation.StringInSlice([]string{"VOLATILE", "IMMUTABLE"}, false), +// Description: "Specifies the behavior of the function when returning results", +// }, +// "api_integration": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "The name of the API integration object that should be used to authenticate the call to the proxy service.", +// }, +// "header": { +// Type: schema.TypeSet, +// Optional: true, +// ForceNew: true, +// Description: "Allows users to specify key-value metadata that is sent with every request as HTTP headers.", +// Elem: &schema.Resource{ +// Schema: map[string]*schema.Schema{ +// "name": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "Header name", +// }, +// "value": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "Header value", +// }, +// }, +// }, +// }, +// "context_headers": { +// Type: schema.TypeList, +// Elem: &schema.Schema{Type: schema.TypeString}, +// Optional: true, +// ForceNew: true, +// // Suppress the diff shown if the values are equal when both compared in lower case. +// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { +// return strings.EqualFold(strings.ToLower(old), strings.ToLower(new)) +// }, +// Description: "Binds Snowflake context function results to HTTP headers.", +// }, +// "max_batch_rows": { +// Type: schema.TypeInt, +// Optional: true, +// ForceNew: true, +// Description: "This specifies the maximum number of rows in each batch sent to the proxy service.", +// }, +// "compression": { +// Type: schema.TypeString, +// Optional: true, +// Default: "AUTO", +// ForceNew: true, +// ValidateFunc: validation.StringInSlice([]string{"NONE", "AUTO", "GZIP", "DEFLATE"}, false), +// Description: "If specified, the JSON payload is compressed when sent from Snowflake to the proxy service, and when sent back from the proxy service to Snowflake.", +// }, +// "request_translator": { +// Type: schema.TypeString, +// Optional: true, +// ForceNew: true, +// Description: "This specifies the name of the request translator function", +// }, +// "response_translator": { +// Type: schema.TypeString, +// Optional: true, +// ForceNew: true, +// Description: "This specifies the name of the response translator function.", +// }, +// "url_of_proxy_and_resource": { +// Type: schema.TypeString, +// Required: true, +// ForceNew: true, +// Description: "This is the invocation URL of the proxy service and resource through which Snowflake calls the remote service.", +// }, +// "comment": { +// Type: schema.TypeString, +// Optional: true, +// Default: "user-defined function", +// Description: "A description of the external function.", +// }, +// "created_on": { +// Type: schema.TypeString, +// Computed: true, +// Description: "Date and time when the external function was created.", +// }, +//} +// +//// ExternalFunction returns a pointer to the resource representing an external function. +//func ExternalFunction() *schema.Resource { +// return &schema.Resource{ +// SchemaVersion: 1, +// +// CreateContext: CreateContextExternalFunction, +// ReadContext: ReadContextExternalFunction, +// UpdateContext: UpdateContextExternalFunction, +// DeleteContext: DeleteContextExternalFunction, +// +// Schema: externalFunctionSchema, +// Importer: &schema.ResourceImporter{ +// StateContext: schema.ImportStatePassthroughContext, +// }, +// +// StateUpgraders: []schema.StateUpgrader{ +// { +// Version: 0, +// // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject +// Type: cty.EmptyObject, +// Upgrade: v085ExternalFunctionStateUpgrader, +// }, +// }, +// } +//} +// +//func CreateContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +// client := meta.(*provider.Context).Client +// database := d.Get("database").(string) +// schemaName := d.Get("schema").(string) +// name := d.Get("name").(string) +// id := sdk.NewSchemaObjectIdentifier(database, schemaName, name) +// +// returnType := d.Get("return_type").(string) +// resultDataType, err := sdk.ToDataType(returnType) +// if err != nil { +// return diag.FromErr(err) +// } +// apiIntegration := sdk.NewAccountObjectIdentifier(d.Get("api_integration").(string)) +// urlOfProxyAndResource := d.Get("url_of_proxy_and_resource").(string) +// req := sdk.NewCreateExternalFunctionRequest(id, resultDataType, &apiIntegration, urlOfProxyAndResource) +// +// // Set optionals +// args := make([]sdk.ExternalFunctionArgumentRequest, 0) +// if v, ok := d.GetOk("arg"); ok { +// for _, arg := range v.([]interface{}) { +// argName := arg.(map[string]interface{})["name"].(string) +// argType := arg.(map[string]interface{})["type"].(string) +// argDataType, err := sdk.ToDataType(argType) +// if err != nil { +// return diag.FromErr(err) +// } +// args = append(args, sdk.ExternalFunctionArgumentRequest{ArgName: argName, ArgDataType: argDataType}) +// } +// } +// if len(args) > 0 { +// req.WithArguments(args) +// } +// +// if v, ok := d.GetOk("return_null_allowed"); ok { +// if v.(bool) { +// req.WithReturnNullValues(&sdk.ReturnNullValuesNull) +// } else { +// req.WithReturnNullValues(&sdk.ReturnNullValuesNotNull) +// } +// } +// +// if v, ok := d.GetOk("return_behavior"); ok { +// if v.(string) == "VOLATILE" { +// req.WithReturnResultsBehavior(&sdk.ReturnResultsBehaviorVolatile) +// } else { +// req.WithReturnResultsBehavior(&sdk.ReturnResultsBehaviorImmutable) +// } +// } +// +// if v, ok := d.GetOk("null_input_behavior"); ok { +// switch { +// case v.(string) == "CALLED ON NULL INPUT": +// req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehaviorCalledOnNullInput)) +// case v.(string) == "RETURNS NULL ON NULL INPUT": +// req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehaviorReturnNullInput)) +// default: +// req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehaviorStrict)) +// } +// } +// +// if v, ok := d.GetOk("comment"); ok { +// req.WithComment(sdk.String(v.(string))) +// } +// +// if _, ok := d.GetOk("header"); ok { +// headers := make([]sdk.ExternalFunctionHeaderRequest, 0) +// for _, header := range d.Get("header").(*schema.Set).List() { +// m := header.(map[string]interface{}) +// headerName := m["name"].(string) +// headerValue := m["value"].(string) +// headers = append(headers, sdk.ExternalFunctionHeaderRequest{ +// Name: headerName, +// Value: headerValue, +// }) +// } +// req.WithHeaders(headers) +// } +// +// if v, ok := d.GetOk("context_headers"); ok { +// contextHeadersList := expandStringList(v.([]interface{})) +// contextHeaders := make([]sdk.ExternalFunctionContextHeaderRequest, 0) +// for _, header := range contextHeadersList { +// contextHeaders = append(contextHeaders, sdk.ExternalFunctionContextHeaderRequest{ +// ContextFunction: header, +// }) +// } +// req.WithContextHeaders(contextHeaders) +// } +// +// if v, ok := d.GetOk("max_batch_rows"); ok { +// req.WithMaxBatchRows(sdk.Int(v.(int))) +// } +// +// if v, ok := d.GetOk("compression"); ok { +// req.WithCompression(sdk.String(v.(string))) +// } +// +// if v, ok := d.GetOk("request_translator"); ok { +// req.WithRequestTranslator(sdk.Pointer(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(v.(string)))) +// } +// +// if v, ok := d.GetOk("response_translator"); ok { +// req.WithResponseTranslator(sdk.Pointer(sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(v.(string)))) +// } +// +// if err := client.ExternalFunctions.Create(ctx, req); err != nil { +// return diag.FromErr(err) +// } +// argTypes := make([]sdk.DataType, 0, len(args)) +// for _, item := range args { +// argTypes = append(argTypes, item.ArgDataType) +// } +// sid := sdk.NewSchemaObjectIdentifierWithArguments(database, schemaName, name, argTypes) +// d.SetId(sid.FullyQualifiedName()) +// return ReadContextExternalFunction(ctx, d, meta) +//} +// +//func ReadContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +// client := meta.(*provider.Context).Client +// +// id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) +// externalFunction, err := client.ExternalFunctions.ShowByID(ctx, id) +// if err != nil { +// d.SetId("") +// return nil +// } +// +// // Some properties can come from the SHOW EXTERNAL FUNCTION call +// if err := d.Set("name", externalFunction.Name); err != nil { +// return diag.FromErr(err) +// } +// +// if err := d.Set("schema", strings.Trim(externalFunction.SchemaName, "\"")); err != nil { +// return diag.FromErr(err) +// } +// +// if err := d.Set("database", strings.Trim(externalFunction.CatalogName, "\"")); err != nil { +// return diag.FromErr(err) +// } +// +// if err := d.Set("comment", externalFunction.Description); err != nil { +// return diag.FromErr(err) +// } +// +// if err := d.Set("created_on", externalFunction.CreatedOn); err != nil { +// return diag.FromErr(err) +// } +// +// // Some properties come from the DESCRIBE FUNCTION call +// externalFunctionPropertyRows, err := client.ExternalFunctions.Describe(ctx, sdk.NewDescribeExternalFunctionRequest(id.WithoutArguments(), id.Arguments())) +// if err != nil { +// d.SetId("") +// return nil +// } +// +// for _, row := range externalFunctionPropertyRows { +// switch row.Property { +// case "signature": +// // Format in Snowflake DB is: (argName argType, argName argType, ...) +// args := strings.ReplaceAll(strings.ReplaceAll(row.Value, "(", ""), ")", "") +// +// if args != "" { // Do nothing for functions without arguments +// argPairs := strings.Split(args, ", ") +// args := []interface{}{} +// +// for _, argPair := range argPairs { +// argItem := strings.Split(argPair, " ") +// +// arg := map[string]interface{}{} +// arg["name"] = argItem[0] +// arg["type"] = argItem[1] +// args = append(args, arg) +// } +// +// if err := d.Set("arg", args); err != nil { +// return diag.Errorf("error setting arg: %v", err) +// } +// } +// case "returns": +// returnType := row.Value +// // We first check for VARIANT or OBJECT +// if returnType == "VARIANT" || returnType == "OBJECT" { +// if err := d.Set("return_type", returnType); err != nil { +// return diag.Errorf("error setting return_type: %v", err) +// } +// break +// } +// +// // otherwise, format in Snowflake DB is returnType() +// re := regexp.MustCompile(`^(\w+)\([0-9]*\)$`) +// match := re.FindStringSubmatch(row.Value) +// if len(match) < 2 { +// return diag.Errorf("return_type %s not recognized", returnType) +// } +// if err := d.Set("return_type", match[1]); err != nil { +// return diag.Errorf("error setting return_type: %v", err) +// } +// +// case "null handling": +// if err := d.Set("null_input_behavior", row.Value); err != nil { +// return diag.Errorf("error setting null_input_behavior: %v", err) +// } +// case "volatility": +// if err := d.Set("return_behavior", row.Value); err != nil { +// return diag.Errorf("error setting return_behavior: %v", err) +// } +// case "headers": +// if row.Value != "" && row.Value != "null" { +// // Format in Snowflake DB is: {"head1":"val1","head2":"val2"} +// var jsonHeaders map[string]string +// err := json.Unmarshal([]byte(row.Value), &jsonHeaders) +// if err != nil { +// return diag.Errorf("error unmarshalling headers: %v", err) +// } +// +// headers := make([]any, 0, len(jsonHeaders)) +// for key, value := range jsonHeaders { +// headers = append(headers, map[string]any{ +// "name": key, +// "value": value, +// }) +// } +// +// if err := d.Set("header", headers); err != nil { +// return diag.Errorf("error setting return_behavior: %v", err) +// } +// } +// case "context_headers": +// if row.Value != "" && row.Value != "null" { +// // Format in Snowflake DB is: ["CONTEXT_FUNCTION_1","CONTEXT_FUNCTION_2"] +// contextHeaders := strings.Split(strings.Trim(row.Value, "[]"), ",") +// for i, v := range contextHeaders { +// contextHeaders[i] = strings.Trim(v, "\"") +// } +// if err := d.Set("context_headers", contextHeaders); err != nil { +// return diag.Errorf("error setting context_headers: %v", err) +// } +// } +// case "max_batch_rows": +// if row.Value != "not set" { +// maxBatchRows, err := strconv.ParseInt(row.Value, 10, 64) +// if err != nil { +// return diag.Errorf("error parsing max_batch_rows: %v", err) +// } +// if err := d.Set("max_batch_rows", maxBatchRows); err != nil { +// return diag.Errorf("error setting max_batch_rows: %v", err) +// } +// } +// case "compression": +// if err := d.Set("compression", row.Value); err != nil { +// return diag.Errorf("error setting compression: %v", err) +// } +// case "body": +// if err := d.Set("url_of_proxy_and_resource", row.Value); err != nil { +// return diag.Errorf("error setting url_of_proxy_and_resource: %v", err) +// } +// case "language": +// // To ignore +// default: +// log.Printf("[WARN] unexpected external function property %v returned from Snowflake", row.Property) +// } +// } +// +// return nil +//} +// +//func UpdateContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +// client := meta.(*provider.Context).Client +// +// id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) +// req := sdk.NewAlterFunctionRequest(id.WithoutArguments(), id.Arguments()) +// if d.HasChange("comment") { +// _, new := d.GetChange("comment") +// if new == "" { +// req.UnsetComment = sdk.Bool(true) +// } else { +// req.SetComment = sdk.String(new.(string)) +// } +// err := client.Functions.Alter(ctx, req) +// if err != nil { +// return diag.FromErr(err) +// } +// } +// return ReadContextExternalFunction(ctx, d, meta) +//} +// +//func DeleteContextExternalFunction(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +// client := meta.(*provider.Context).Client +// +// id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) +// req := sdk.NewDropFunctionRequest(id.WithoutArguments(), id.Arguments()) +// if err := client.Functions.Drop(ctx, req); err != nil { +// return diag.FromErr(err) +// } +// +// d.SetId("") +// return nil +//} diff --git a/pkg/resources/external_function_acceptance_test.go b/pkg/resources/external_function_acceptance_test.go index 66513f390d..ecb8c43f11 100644 --- a/pkg/resources/external_function_acceptance_test.go +++ b/pkg/resources/external_function_acceptance_test.go @@ -1,562 +1,548 @@ package resources_test -import ( - "fmt" - "testing" - - acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestAcc_ExternalFunction_basic(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() - - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "name": config.StringVariable(accName), - "api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")), - "url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), - "comment": config.StringVariable("Terraform acceptance test"), - } - } - - resourceName := "snowflake_external_function.external_function" - configVariables := m() - configVariables2 := m() - configVariables2["comment"] = config.StringVariable("Terraform acceptance test - updated") - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - Steps: []resource.TestStep{ - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/basic"), - ConfigVariables: configVariables, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", accName), - resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr(resourceName, "arg.#", "2"), - resource.TestCheckResourceAttr(resourceName, "arg.0.name", "ARG1"), - resource.TestCheckResourceAttr(resourceName, "arg.0.type", "VARCHAR"), - resource.TestCheckResourceAttr(resourceName, "arg.1.name", "ARG2"), - resource.TestCheckResourceAttr(resourceName, "arg.1.type", "VARCHAR"), - resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "CALLED ON NULL INPUT"), - resource.TestCheckResourceAttr(resourceName, "return_type", "VARIANT"), - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - resource.TestCheckResourceAttr(resourceName, "return_behavior", "IMMUTABLE"), - resource.TestCheckResourceAttrSet(resourceName, "api_integration"), - resource.TestCheckResourceAttr(resourceName, "compression", "AUTO"), - resource.TestCheckResourceAttr(resourceName, "url_of_proxy_and_resource", "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), - resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), - resource.TestCheckResourceAttrSet(resourceName, "created_on"), - ), - }, - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/basic"), - ConfigVariables: configVariables2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), - ), - }, - // IMPORT - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/basic"), - ConfigVariables: configVariables2, - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - // these two are not found in either the show or describe command - ImportStateVerifyIgnore: []string{"return_null_allowed", "api_integration"}, - }, - }, - }) -} - -func TestAcc_ExternalFunction_no_arguments(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() - - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "name": config.StringVariable(accName), - "api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")), - "url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), - "comment": config.StringVariable("Terraform acceptance test"), - } - } - - resourceName := "snowflake_external_function.external_function" - configVariables := m() - configVariables2 := m() - configVariables2["comment"] = config.StringVariable("Terraform acceptance test - updated") - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - Steps: []resource.TestStep{ - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/no_arguments"), - ConfigVariables: configVariables, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", accName), - resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr(resourceName, "arg.#", "0"), - resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "CALLED ON NULL INPUT"), - resource.TestCheckResourceAttr(resourceName, "return_type", "VARIANT"), - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - resource.TestCheckResourceAttr(resourceName, "return_behavior", "IMMUTABLE"), - resource.TestCheckResourceAttrSet(resourceName, "api_integration"), - resource.TestCheckResourceAttr(resourceName, "compression", "AUTO"), - resource.TestCheckResourceAttr(resourceName, "url_of_proxy_and_resource", "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), - resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), - resource.TestCheckResourceAttrSet(resourceName, "created_on"), - ), - }, - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/no_arguments"), - ConfigVariables: configVariables2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), - ), - }, - // IMPORT - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/no_arguments"), - ConfigVariables: configVariables2, - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - // these two are not found in either the show or describe command - ImportStateVerifyIgnore: []string{"return_null_allowed", "api_integration"}, - }, - }, - }) -} - -func TestAcc_ExternalFunction_complete(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() - - m := func() map[string]config.Variable { - return map[string]config.Variable{ - "database": config.StringVariable(acc.TestDatabaseName), - "schema": config.StringVariable(acc.TestSchemaName), - "name": config.StringVariable(accName), - "api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")), - "url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), - "comment": config.StringVariable("Terraform acceptance test"), - } - } - - resourceName := "snowflake_external_function.external_function" - configVariables := m() - configVariables2 := m() - configVariables2["comment"] = config.StringVariable("Terraform acceptance test - updated") - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - Steps: []resource.TestStep{ - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/complete"), - ConfigVariables: configVariables, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", accName), - resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr(resourceName, "arg.#", "0"), - resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "CALLED ON NULL INPUT"), - resource.TestCheckResourceAttr(resourceName, "return_type", "VARIANT"), - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - resource.TestCheckResourceAttr(resourceName, "return_behavior", "IMMUTABLE"), - resource.TestCheckResourceAttrSet(resourceName, "api_integration"), - resource.TestCheckResourceAttr(resourceName, "compression", "AUTO"), - resource.TestCheckResourceAttr(resourceName, "url_of_proxy_and_resource", "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), - resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), - resource.TestCheckResourceAttrSet(resourceName, "created_on"), - resource.TestCheckResourceAttr(resourceName, "header.#", "1"), - resource.TestCheckResourceAttr(resourceName, "header.0.name", "x-custom-header"), - resource.TestCheckResourceAttr(resourceName, "header.0.value", "snowflake"), - resource.TestCheckResourceAttr(resourceName, "max_batch_rows", "500"), - resource.TestCheckResourceAttr(resourceName, "request_translator", fmt.Sprintf("%s.%s.%s%s", acc.TestDatabaseName, acc.TestSchemaName, accName, "_request_translator")), - resource.TestCheckResourceAttr(resourceName, "response_translator", fmt.Sprintf("%s.%s.%s%s", acc.TestDatabaseName, acc.TestSchemaName, accName, "_response_translator")), - ), - }, - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/complete"), - ConfigVariables: configVariables2, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), - ), - }, - // IMPORT - { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/complete"), - ConfigVariables: configVariables2, - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - // these four are not found in either the show or describe command - ImportStateVerifyIgnore: []string{"return_null_allowed", "api_integration", "request_translator", "response_translator"}, - }, - }, - }) -} - -func TestAcc_ExternalFunction_migrateFromVersion085(t *testing.T) { - id := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithArguments([]sdk.DataType{sdk.DataTypeVARCHAR, sdk.DataTypeVARCHAR}) - name := id.Name() - resourceName := "snowflake_external_function.f" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - - Steps: []resource.TestStep{ - { - ExternalProviders: map[string]resource.ExternalProvider{ - "snowflake": { - VersionConstraint: "=0.85.0", - Source: "Snowflake-Labs/snowflake", - }, - }, - Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|%s|%s|VARCHAR-VARCHAR", acc.TestDatabaseName, acc.TestSchemaName, name)), - resource.TestCheckResourceAttr(resourceName, "name", name), - resource.TestCheckResourceAttr(resourceName, "database", "\""+acc.TestDatabaseName+"\""), - resource.TestCheckResourceAttr(resourceName, "schema", "\""+acc.TestSchemaName+"\""), - resource.TestCheckNoResourceAttr(resourceName, "return_null_allowed"), - ), - ExpectNonEmptyPlan: true, - }, - { - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, - }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "id", id.FullyQualifiedName()), - resource.TestCheckResourceAttr(resourceName, "name", name), - resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), - resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - ), - }, - }, - }) -} - -func TestAcc_ExternalFunction_migrateFromVersion085_issue2694_previousValuePresent(t *testing.T) { - name := acc.TestClient().Ids.Alpha() - resourceName := "snowflake_external_function.f" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - - Steps: []resource.TestStep{ - { - ExternalProviders: map[string]resource.ExternalProvider{ - "snowflake": { - VersionConstraint: "=0.85.0", - Source: "Snowflake-Labs/snowflake", - }, - }, - Config: externalFunctionConfigWithReturnNullAllowed(acc.TestDatabaseName, acc.TestSchemaName, name, sdk.Bool(true)), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - ), - ExpectNonEmptyPlan: true, - }, - { - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, - }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - ), - }, - }, - }) -} - -func TestAcc_ExternalFunction_migrateFromVersion085_issue2694_previousValueRemoved(t *testing.T) { - name := acc.TestClient().Ids.Alpha() - resourceName := "snowflake_external_function.f" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - - Steps: []resource.TestStep{ - { - ExternalProviders: map[string]resource.ExternalProvider{ - "snowflake": { - VersionConstraint: "=0.85.0", - Source: "Snowflake-Labs/snowflake", - }, - }, - Config: externalFunctionConfigWithReturnNullAllowed(acc.TestDatabaseName, acc.TestSchemaName, name, sdk.Bool(true)), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - ), - ExpectNonEmptyPlan: true, - }, - { - ExternalProviders: map[string]resource.ExternalProvider{ - "snowflake": { - VersionConstraint: "=0.85.0", - Source: "Snowflake-Labs/snowflake", - }, - }, - Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckNoResourceAttr(resourceName, "return_null_allowed"), - ), - ExpectNonEmptyPlan: true, - }, - { - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, - }, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), - ), - }, - }, - }) -} - -// Proves issue https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2528. -// The problem originated from ShowById without IN clause. There was no IN clause in the docs at the time. -// It was raised with the appropriate team in Snowflake. -func TestAcc_ExternalFunction_issue2528(t *testing.T) { - accName := acc.TestClient().Ids.Alpha() - secondSchema := acc.TestClient().Ids.Alpha() - - resourceName := "snowflake_external_function.f" - - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - Steps: []resource.TestStep{ - { - Config: externalFunctionConfigIssue2528(acc.TestDatabaseName, acc.TestSchemaName, accName, secondSchema), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", accName), - ), - }, - }, - }) -} - -// Proves that header parsing handles values wrapped in curly braces, e.g. `value = "{1}"` -func TestAcc_ExternalFunction_HeaderParsing(t *testing.T) { - id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() - - resourceName := "snowflake_external_function.f" - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acc.TestAccPreCheck(t) }, - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.RequireAbove(tfversion.Version1_5_0), - }, - CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), - Steps: []resource.TestStep{ - { - ExternalProviders: map[string]resource.ExternalProvider{ - "snowflake": { - VersionConstraint: "=0.93.0", - Source: "Snowflake-Labs/snowflake", - }, - }, - Config: externalFunctionConfigIssueCurlyHeader(id), - // Previous implementation produces a plan with the following changes - // - // - header { # forces replacement - // - name = "name" -> null - // - value = "0" -> null - // } - // - // + header { # forces replacement - // + name = "name" - // + value = "{0}" - // } - ExpectNonEmptyPlan: true, - }, - { - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: externalFunctionConfigIssueCurlyHeader(id), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "header.#", "1"), - resource.TestCheckResourceAttr(resourceName, "header.0.name", "name"), - resource.TestCheckResourceAttr(resourceName, "header.0.value", "{0}"), - ), - }, - }, - }) -} - -func externalFunctionConfig(database string, schema string, name string) string { - return externalFunctionConfigWithReturnNullAllowed(database, schema, name, nil) -} - -func externalFunctionConfigWithReturnNullAllowed(database string, schema string, name string, returnNullAllowed *bool) string { - returnNullAllowedText := "" - if returnNullAllowed != nil { - returnNullAllowedText = fmt.Sprintf("return_null_allowed = \"%t\"", *returnNullAllowed) - } - - return fmt.Sprintf(` -resource "snowflake_api_integration" "test_api_int" { - name = "%[3]s" - api_provider = "aws_api_gateway" - api_aws_role_arn = "arn:aws:iam::000000000001:/role/test" - api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"] - enabled = true -} - -resource "snowflake_external_function" "f" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - arg { - name = "ARG1" - type = "VARCHAR" - } - arg { - name = "ARG2" - type = "VARCHAR" - } - return_type = "VARIANT" - return_behavior = "IMMUTABLE" - api_integration = snowflake_api_integration.test_api_int.name - url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" - %[4]s -} - -`, database, schema, name, returnNullAllowedText) -} - -func externalFunctionConfigIssue2528(database string, schema string, name string, schema2 string) string { - return fmt.Sprintf(` -resource "snowflake_api_integration" "test_api_int" { - name = "%[3]s" - api_provider = "aws_api_gateway" - api_aws_role_arn = "arn:aws:iam::000000000001:/role/test" - api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"] - enabled = true -} - -resource "snowflake_schema" "s2" { - database = "%[1]s" - name = "%[4]s" -} - -resource "snowflake_external_function" "f" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - arg { - name = "SNS_NOTIF" - type = "OBJECT" - } - return_type = "VARIANT" - return_behavior = "VOLATILE" - api_integration = snowflake_api_integration.test_api_int.name - url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" -} - -resource "snowflake_external_function" "f2" { - depends_on = [snowflake_schema.s2] - - name = "%[3]s" - database = "%[1]s" - schema = "%[4]s" - arg { - name = "SNS_NOTIF" - type = "OBJECT" - } - return_type = "VARIANT" - return_behavior = "VOLATILE" - api_integration = snowflake_api_integration.test_api_int.name - url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" -} -`, database, schema, name, schema2) -} - -func externalFunctionConfigIssueCurlyHeader(id sdk.SchemaObjectIdentifier) string { - return fmt.Sprintf(` -resource "snowflake_api_integration" "test_api_int" { - name = "%[3]s" - api_provider = "aws_api_gateway" - api_aws_role_arn = "arn:aws:iam::000000000001:/role/test" - api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"] - enabled = true -} - -resource "snowflake_external_function" "f" { - name = "%[3]s" - database = "%[1]s" - schema = "%[2]s" - arg { - name = "ARG1" - type = "VARCHAR" - } - arg { - name = "ARG2" - type = "VARCHAR" - } - header { - name = "name" - value = "{0}" - } - return_type = "VARIANT" - return_behavior = "IMMUTABLE" - api_integration = snowflake_api_integration.test_api_int.name - url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" -} - -`, id.DatabaseName(), id.SchemaName(), id.Name()) -} +//func TestAcc_ExternalFunction_basic(t *testing.T) { +// accName := acc.TestClient().Ids.Alpha() +// +// m := func() map[string]config.Variable { +// return map[string]config.Variable{ +// "database": config.StringVariable(acc.TestDatabaseName), +// "schema": config.StringVariable(acc.TestSchemaName), +// "name": config.StringVariable(accName), +// "api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")), +// "url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), +// "comment": config.StringVariable("Terraform acceptance test"), +// } +// } +// +// resourceName := "snowflake_external_function.external_function" +// configVariables := m() +// configVariables2 := m() +// configVariables2["comment"] = config.StringVariable("Terraform acceptance test - updated") +// +// resource.Test(t, resource.TestCase{ +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// Steps: []resource.TestStep{ +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/basic"), +// ConfigVariables: configVariables, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "name", accName), +// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), +// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), +// resource.TestCheckResourceAttr(resourceName, "arg.#", "2"), +// resource.TestCheckResourceAttr(resourceName, "arg.0.name", "ARG1"), +// resource.TestCheckResourceAttr(resourceName, "arg.0.type", "VARCHAR"), +// resource.TestCheckResourceAttr(resourceName, "arg.1.name", "ARG2"), +// resource.TestCheckResourceAttr(resourceName, "arg.1.type", "VARCHAR"), +// resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "CALLED ON NULL INPUT"), +// resource.TestCheckResourceAttr(resourceName, "return_type", "VARIANT"), +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// resource.TestCheckResourceAttr(resourceName, "return_behavior", "IMMUTABLE"), +// resource.TestCheckResourceAttrSet(resourceName, "api_integration"), +// resource.TestCheckResourceAttr(resourceName, "compression", "AUTO"), +// resource.TestCheckResourceAttr(resourceName, "url_of_proxy_and_resource", "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), +// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), +// resource.TestCheckResourceAttrSet(resourceName, "created_on"), +// ), +// }, +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/basic"), +// ConfigVariables: configVariables2, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), +// ), +// }, +// // IMPORT +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/basic"), +// ConfigVariables: configVariables2, +// ResourceName: resourceName, +// ImportState: true, +// ImportStateVerify: true, +// // these two are not found in either the show or describe command +// ImportStateVerifyIgnore: []string{"return_null_allowed", "api_integration"}, +// }, +// }, +// }) +//} +// +//func TestAcc_ExternalFunction_no_arguments(t *testing.T) { +// accName := acc.TestClient().Ids.Alpha() +// +// m := func() map[string]config.Variable { +// return map[string]config.Variable{ +// "database": config.StringVariable(acc.TestDatabaseName), +// "schema": config.StringVariable(acc.TestSchemaName), +// "name": config.StringVariable(accName), +// "api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")), +// "url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), +// "comment": config.StringVariable("Terraform acceptance test"), +// } +// } +// +// resourceName := "snowflake_external_function.external_function" +// configVariables := m() +// configVariables2 := m() +// configVariables2["comment"] = config.StringVariable("Terraform acceptance test - updated") +// +// resource.Test(t, resource.TestCase{ +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// Steps: []resource.TestStep{ +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/no_arguments"), +// ConfigVariables: configVariables, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "name", accName), +// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), +// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), +// resource.TestCheckResourceAttr(resourceName, "arg.#", "0"), +// resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "CALLED ON NULL INPUT"), +// resource.TestCheckResourceAttr(resourceName, "return_type", "VARIANT"), +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// resource.TestCheckResourceAttr(resourceName, "return_behavior", "IMMUTABLE"), +// resource.TestCheckResourceAttrSet(resourceName, "api_integration"), +// resource.TestCheckResourceAttr(resourceName, "compression", "AUTO"), +// resource.TestCheckResourceAttr(resourceName, "url_of_proxy_and_resource", "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), +// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), +// resource.TestCheckResourceAttrSet(resourceName, "created_on"), +// ), +// }, +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/no_arguments"), +// ConfigVariables: configVariables2, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), +// ), +// }, +// // IMPORT +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/no_arguments"), +// ConfigVariables: configVariables2, +// ResourceName: resourceName, +// ImportState: true, +// ImportStateVerify: true, +// // these two are not found in either the show or describe command +// ImportStateVerifyIgnore: []string{"return_null_allowed", "api_integration"}, +// }, +// }, +// }) +//} +// +//func TestAcc_ExternalFunction_complete(t *testing.T) { +// accName := acc.TestClient().Ids.Alpha() +// +// m := func() map[string]config.Variable { +// return map[string]config.Variable{ +// "database": config.StringVariable(acc.TestDatabaseName), +// "schema": config.StringVariable(acc.TestSchemaName), +// "name": config.StringVariable(accName), +// "api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")), +// "url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), +// "comment": config.StringVariable("Terraform acceptance test"), +// } +// } +// +// resourceName := "snowflake_external_function.external_function" +// configVariables := m() +// configVariables2 := m() +// configVariables2["comment"] = config.StringVariable("Terraform acceptance test - updated") +// +// resource.Test(t, resource.TestCase{ +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// Steps: []resource.TestStep{ +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/complete"), +// ConfigVariables: configVariables, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "name", accName), +// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), +// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), +// resource.TestCheckResourceAttr(resourceName, "arg.#", "0"), +// resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "CALLED ON NULL INPUT"), +// resource.TestCheckResourceAttr(resourceName, "return_type", "VARIANT"), +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// resource.TestCheckResourceAttr(resourceName, "return_behavior", "IMMUTABLE"), +// resource.TestCheckResourceAttrSet(resourceName, "api_integration"), +// resource.TestCheckResourceAttr(resourceName, "compression", "AUTO"), +// resource.TestCheckResourceAttr(resourceName, "url_of_proxy_and_resource", "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"), +// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), +// resource.TestCheckResourceAttrSet(resourceName, "created_on"), +// resource.TestCheckResourceAttr(resourceName, "header.#", "1"), +// resource.TestCheckResourceAttr(resourceName, "header.0.name", "x-custom-header"), +// resource.TestCheckResourceAttr(resourceName, "header.0.value", "snowflake"), +// resource.TestCheckResourceAttr(resourceName, "max_batch_rows", "500"), +// resource.TestCheckResourceAttr(resourceName, "request_translator", fmt.Sprintf("%s.%s.%s%s", acc.TestDatabaseName, acc.TestSchemaName, accName, "_request_translator")), +// resource.TestCheckResourceAttr(resourceName, "response_translator", fmt.Sprintf("%s.%s.%s%s", acc.TestDatabaseName, acc.TestSchemaName, accName, "_response_translator")), +// ), +// }, +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/complete"), +// ConfigVariables: configVariables2, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), +// ), +// }, +// // IMPORT +// { +// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunction/complete"), +// ConfigVariables: configVariables2, +// ResourceName: resourceName, +// ImportState: true, +// ImportStateVerify: true, +// // these four are not found in either the show or describe command +// ImportStateVerifyIgnore: []string{"return_null_allowed", "api_integration", "request_translator", "response_translator"}, +// }, +// }, +// }) +//} +// +//func TestAcc_ExternalFunction_migrateFromVersion085(t *testing.T) { +// id := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithArguments([]sdk.DataType{sdk.DataTypeVARCHAR, sdk.DataTypeVARCHAR}) +// name := id.Name() +// resourceName := "snowflake_external_function.f" +// +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// +// Steps: []resource.TestStep{ +// { +// ExternalProviders: map[string]resource.ExternalProvider{ +// "snowflake": { +// VersionConstraint: "=0.85.0", +// Source: "Snowflake-Labs/snowflake", +// }, +// }, +// Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|%s|%s|VARCHAR-VARCHAR", acc.TestDatabaseName, acc.TestSchemaName, name)), +// resource.TestCheckResourceAttr(resourceName, "name", name), +// resource.TestCheckResourceAttr(resourceName, "database", "\""+acc.TestDatabaseName+"\""), +// resource.TestCheckResourceAttr(resourceName, "schema", "\""+acc.TestSchemaName+"\""), +// resource.TestCheckNoResourceAttr(resourceName, "return_null_allowed"), +// ), +// ExpectNonEmptyPlan: true, +// }, +// { +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), +// ConfigPlanChecks: resource.ConfigPlanChecks{ +// PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, +// }, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "id", id.FullyQualifiedName()), +// resource.TestCheckResourceAttr(resourceName, "name", name), +// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), +// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// ), +// }, +// }, +// }) +//} +// +//func TestAcc_ExternalFunction_migrateFromVersion085_issue2694_previousValuePresent(t *testing.T) { +// name := acc.TestClient().Ids.Alpha() +// resourceName := "snowflake_external_function.f" +// +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// +// Steps: []resource.TestStep{ +// { +// ExternalProviders: map[string]resource.ExternalProvider{ +// "snowflake": { +// VersionConstraint: "=0.85.0", +// Source: "Snowflake-Labs/snowflake", +// }, +// }, +// Config: externalFunctionConfigWithReturnNullAllowed(acc.TestDatabaseName, acc.TestSchemaName, name, sdk.Bool(true)), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// ), +// ExpectNonEmptyPlan: true, +// }, +// { +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), +// ConfigPlanChecks: resource.ConfigPlanChecks{ +// PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, +// }, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// ), +// }, +// }, +// }) +//} +// +//func TestAcc_ExternalFunction_migrateFromVersion085_issue2694_previousValueRemoved(t *testing.T) { +// name := acc.TestClient().Ids.Alpha() +// resourceName := "snowflake_external_function.f" +// +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// +// Steps: []resource.TestStep{ +// { +// ExternalProviders: map[string]resource.ExternalProvider{ +// "snowflake": { +// VersionConstraint: "=0.85.0", +// Source: "Snowflake-Labs/snowflake", +// }, +// }, +// Config: externalFunctionConfigWithReturnNullAllowed(acc.TestDatabaseName, acc.TestSchemaName, name, sdk.Bool(true)), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// ), +// ExpectNonEmptyPlan: true, +// }, +// { +// ExternalProviders: map[string]resource.ExternalProvider{ +// "snowflake": { +// VersionConstraint: "=0.85.0", +// Source: "Snowflake-Labs/snowflake", +// }, +// }, +// Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckNoResourceAttr(resourceName, "return_null_allowed"), +// ), +// ExpectNonEmptyPlan: true, +// }, +// { +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// Config: externalFunctionConfig(acc.TestDatabaseName, acc.TestSchemaName, name), +// ConfigPlanChecks: resource.ConfigPlanChecks{ +// PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, +// }, +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "return_null_allowed", "true"), +// ), +// }, +// }, +// }) +//} +// +//// Proves issue https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2528. +//// The problem originated from ShowById without IN clause. There was no IN clause in the docs at the time. +//// It was raised with the appropriate team in Snowflake. +//func TestAcc_ExternalFunction_issue2528(t *testing.T) { +// accName := acc.TestClient().Ids.Alpha() +// secondSchema := acc.TestClient().Ids.Alpha() +// +// resourceName := "snowflake_external_function.f" +// +// resource.Test(t, resource.TestCase{ +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// Steps: []resource.TestStep{ +// { +// Config: externalFunctionConfigIssue2528(acc.TestDatabaseName, acc.TestSchemaName, accName, secondSchema), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "name", accName), +// ), +// }, +// }, +// }) +//} +// +//// Proves that header parsing handles values wrapped in curly braces, e.g. `value = "{1}"` +//func TestAcc_ExternalFunction_HeaderParsing(t *testing.T) { +// id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() +// +// resourceName := "snowflake_external_function.f" +// +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { acc.TestAccPreCheck(t) }, +// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ +// tfversion.RequireAbove(tfversion.Version1_5_0), +// }, +// CheckDestroy: acc.CheckDestroy(t, resources.ExternalFunction), +// Steps: []resource.TestStep{ +// { +// ExternalProviders: map[string]resource.ExternalProvider{ +// "snowflake": { +// VersionConstraint: "=0.93.0", +// Source: "Snowflake-Labs/snowflake", +// }, +// }, +// Config: externalFunctionConfigIssueCurlyHeader(id), +// // Previous implementation produces a plan with the following changes +// // +// // - header { # forces replacement +// // - name = "name" -> null +// // - value = "0" -> null +// // } +// // +// // + header { # forces replacement +// // + name = "name" +// // + value = "{0}" +// // } +// ExpectNonEmptyPlan: true, +// }, +// { +// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, +// Config: externalFunctionConfigIssueCurlyHeader(id), +// Check: resource.ComposeTestCheckFunc( +// resource.TestCheckResourceAttr(resourceName, "header.#", "1"), +// resource.TestCheckResourceAttr(resourceName, "header.0.name", "name"), +// resource.TestCheckResourceAttr(resourceName, "header.0.value", "{0}"), +// ), +// }, +// }, +// }) +//} +// +//func externalFunctionConfig(database string, schema string, name string) string { +// return externalFunctionConfigWithReturnNullAllowed(database, schema, name, nil) +//} +// +//func externalFunctionConfigWithReturnNullAllowed(database string, schema string, name string, returnNullAllowed *bool) string { +// returnNullAllowedText := "" +// if returnNullAllowed != nil { +// returnNullAllowedText = fmt.Sprintf("return_null_allowed = \"%t\"", *returnNullAllowed) +// } +// +// return fmt.Sprintf(` +//resource "snowflake_api_integration" "test_api_int" { +// name = "%[3]s" +// api_provider = "aws_api_gateway" +// api_aws_role_arn = "arn:aws:iam::000000000001:/role/test" +// api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"] +// enabled = true +//} +// +//resource "snowflake_external_function" "f" { +// name = "%[3]s" +// database = "%[1]s" +// schema = "%[2]s" +// arg { +// name = "ARG1" +// type = "VARCHAR" +// } +// arg { +// name = "ARG2" +// type = "VARCHAR" +// } +// return_type = "VARIANT" +// return_behavior = "IMMUTABLE" +// api_integration = snowflake_api_integration.test_api_int.name +// url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" +// %[4]s +//} +// +//`, database, schema, name, returnNullAllowedText) +//} +// +//func externalFunctionConfigIssue2528(database string, schema string, name string, schema2 string) string { +// return fmt.Sprintf(` +//resource "snowflake_api_integration" "test_api_int" { +// name = "%[3]s" +// api_provider = "aws_api_gateway" +// api_aws_role_arn = "arn:aws:iam::000000000001:/role/test" +// api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"] +// enabled = true +//} +// +//resource "snowflake_schema" "s2" { +// database = "%[1]s" +// name = "%[4]s" +//} +// +//resource "snowflake_external_function" "f" { +// name = "%[3]s" +// database = "%[1]s" +// schema = "%[2]s" +// arg { +// name = "SNS_NOTIF" +// type = "OBJECT" +// } +// return_type = "VARIANT" +// return_behavior = "VOLATILE" +// api_integration = snowflake_api_integration.test_api_int.name +// url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" +//} +// +//resource "snowflake_external_function" "f2" { +// depends_on = [snowflake_schema.s2] +// +// name = "%[3]s" +// database = "%[1]s" +// schema = "%[4]s" +// arg { +// name = "SNS_NOTIF" +// type = "OBJECT" +// } +// return_type = "VARIANT" +// return_behavior = "VOLATILE" +// api_integration = snowflake_api_integration.test_api_int.name +// url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" +//} +//`, database, schema, name, schema2) +//} +// +//func externalFunctionConfigIssueCurlyHeader(id sdk.SchemaObjectIdentifier) string { +// return fmt.Sprintf(` +//resource "snowflake_api_integration" "test_api_int" { +// name = "%[3]s" +// api_provider = "aws_api_gateway" +// api_aws_role_arn = "arn:aws:iam::000000000001:/role/test" +// api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"] +// enabled = true +//} +// +//resource "snowflake_external_function" "f" { +// name = "%[3]s" +// database = "%[1]s" +// schema = "%[2]s" +// arg { +// name = "ARG1" +// type = "VARCHAR" +// } +// arg { +// name = "ARG2" +// type = "VARCHAR" +// } +// header { +// name = "name" +// value = "{0}" +// } +// return_type = "VARIANT" +// return_behavior = "IMMUTABLE" +// api_integration = snowflake_api_integration.test_api_int.name +// url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func" +//} +// +//`, id.DatabaseName(), id.SchemaName(), id.Name()) +//} diff --git a/pkg/resources/function_acceptance_test.go b/pkg/resources/function_acceptance_test.go index fe32ca0f76..2a41bff811 100644 --- a/pkg/resources/function_acceptance_test.go +++ b/pkg/resources/function_acceptance_test.go @@ -2,6 +2,7 @@ package resources_test import ( "fmt" + "regexp" "strings" "testing" @@ -218,8 +219,13 @@ func TestAcc_Function_migrateFromVersion085(t *testing.T) { ), }, { - ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - Config: functionConfig(acc.TestDatabaseName, acc.TestSchemaName, name, comment), + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.94.1", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: functionConfig(acc.TestDatabaseName, acc.TestSchemaName, name, comment), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "id", id.FullyQualifiedName()), resource.TestCheckResourceAttr(resourceName, "name", name), @@ -231,6 +237,41 @@ func TestAcc_Function_migrateFromVersion085(t *testing.T) { }) } +func TestAcc_Function_Version0941_ResourceIdMigration(t *testing.T) { + name := acc.TestClient().Ids.RandomAccountObjectIdentifier().Name() + resourceName := "snowflake_function.f" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Function), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.94.1", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: functionConfigWithVector(acc.TestDatabaseName, acc.TestSchemaName, name, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%s"."%s"."%s"(VARCHAR, VECTOR(INT, 20), FLOAT, NUMBER, VECTOR(FLOAT, 10))`, acc.TestDatabaseName, acc.TestSchemaName, name)), + ), + ExpectError: regexp.MustCompile("Error: invalid data type: VECTOR\\(INT, 20\\)"), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: functionConfigWithVector(acc.TestDatabaseName, acc.TestSchemaName, name, ""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%s"."%s"."%s"(VARCHAR, VECTOR(INT, 20), FLOAT, NUMBER, VECTOR(FLOAT, 10))`, acc.TestDatabaseName, acc.TestSchemaName, name)), + ), + }, + }, + }) +} + func TestAcc_Function_Rename(t *testing.T) { name := acc.TestClient().Ids.Alpha() newName := acc.TestClient().Ids.Alpha() @@ -272,6 +313,41 @@ func TestAcc_Function_Rename(t *testing.T) { }) } +func functionConfigWithVector(database string, schema string, name string, comment string) string { + return fmt.Sprintf(` +resource "snowflake_function" "f" { + database = "%[1]s" + schema = "%[2]s" + name = "%[3]s" + comment = "%[4]s" + return_type = "VARCHAR" + return_behavior = "IMMUTABLE" + statement = "SELECT A" + + arguments { + name = "A" + type = "VARCHAR(200)" + } + arguments { + name = "B" + type = "VECTOR(INT, 20)" + } + arguments { + name = "C" + type = "FLOAT" + } + arguments { + name = "D" + type = "NUMBER(10, 2)" + } + arguments { + name = "E" + type = "VECTOR(FLOAT, 10)" + } +} +`, database, schema, name, comment) +} + func functionConfig(database string, schema string, name string, comment string) string { return fmt.Sprintf(` resource "snowflake_function" "f" { diff --git a/pkg/resources/function_state_upgraders.go b/pkg/resources/function_state_upgraders.go index 26e6debc1e..ac7c1bd20a 100644 --- a/pkg/resources/function_state_upgraders.go +++ b/pkg/resources/function_state_upgraders.go @@ -33,6 +33,7 @@ func parseV085FunctionId(v string) (*v085FunctionId, error) { ArgTypes: args, }, nil } + func v085FunctionIdStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { if rawState == nil { return rawState, nil diff --git a/pkg/sdk/functions_gen.go b/pkg/sdk/functions_gen.go index 81d9a38614..6ff4dc9632 100644 --- a/pkg/sdk/functions_gen.go +++ b/pkg/sdk/functions_gen.go @@ -1,12 +1,9 @@ package sdk import ( - "bytes" "context" "database/sql" - "fmt" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" - "log" "strings" ) @@ -226,15 +223,15 @@ type functionRow struct { } type Function struct { - CreatedOn string - Name string - SchemaName string - IsBuiltin bool - IsAggregate bool - IsAnsi bool - MinNumArguments int - MaxNumArguments int - // TODO(SNOW-function refactor): Remove raw arguments + CreatedOn string + Name string + SchemaName string + IsBuiltin bool + IsAggregate bool + IsAnsi bool + MinNumArguments int + MaxNumArguments int + Arguments []DataType ArgumentsRaw string Description string CatalogName string @@ -264,54 +261,8 @@ func parseFunctionArgumentsFromDetails(details []FunctionDetail) ([]DataType, er return arguments, nil } -// Move to sdk/identifier_parsers.go -func parseFunctionArgumentsFromString(arguments string) ([]DataType, error) { - dataTypes := make([]DataType, 0) - - stringBuffer := bytes.NewBufferString(arguments) - for stringBuffer.Len() > 0 { - - // we use another buffer to peek into next data type - peekBuffer := bytes.NewBufferString(stringBuffer.String()) - peekDataType, _ := peekBuffer.ReadString(',') - peekDataType = strings.TrimSpace(peekDataType) - - // For function purposes only Vector needs special case - switch { - case strings.HasPrefix(peekDataType, "VECTOR"): - vectorDataType, _ := stringBuffer.ReadString(')') - vectorDataType = strings.TrimSpace(vectorDataType) - if stringBuffer.Len() > 0 { - commaByte, err := stringBuffer.ReadByte() - if commaByte != ',' { - return nil, fmt.Errorf("expected a comma delimited string but found %s", string(commaByte)) - } - if err != nil { - return nil, err - } - } - log.Println("Adding vec:", vectorDataType) - dataTypes = append(dataTypes, DataType(vectorDataType)) - default: - dataType, err := stringBuffer.ReadString(',') - if err == nil { - dataType = dataType[:len(dataType)-1] - } - dataType = strings.TrimSpace(dataType) - log.Println("Adding:", dataType) - dataTypes = append(dataTypes, DataType(dataType)) - } - } - - return dataTypes, nil -} - -func (v *Function) ID(details []FunctionDetail) (SchemaObjectIdentifierWithArguments, error) { - arguments, err := parseFunctionArgumentsFromDetails(details) - if err != nil { - return SchemaObjectIdentifierWithArguments{}, err - } - return NewSchemaObjectIdentifierWithArguments(v.CatalogName, v.SchemaName, v.Name, arguments...), nil +func (v *Function) ID() SchemaObjectIdentifierWithArguments { + return NewSchemaObjectIdentifierWithArguments(v.CatalogName, v.SchemaName, v.Name, v.Arguments...) } // DescribeFunctionOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-function. diff --git a/pkg/sdk/functions_impl_gen.go b/pkg/sdk/functions_impl_gen.go index 3fca482739..a082969111 100644 --- a/pkg/sdk/functions_impl_gen.go +++ b/pkg/sdk/functions_impl_gen.go @@ -3,6 +3,8 @@ package sdk import ( "context" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" + "log" + "strings" ) var _ Functions = (*functions)(nil) @@ -379,6 +381,15 @@ func (r functionRow) convert() *Function { IsExternalFunction: r.IsExternalFunction == "Y", Language: r.Language, } + arguments := strings.TrimLeft(r.Arguments, r.Name) + returnIndex := strings.Index(arguments, ") RETURN ") + dataTypes, err := ParseFunctionArgumentsFromString(arguments[:returnIndex+1]) + if err != nil { + log.Printf("[DEBUG] failed to parse function arguments, err = %s", err) + } else { + e.Arguments = dataTypes + } + if r.IsSecure.Valid { e.IsSecure = r.IsSecure.String == "Y" } diff --git a/pkg/sdk/identifier_helpers.go b/pkg/sdk/identifier_helpers.go index 1ca037b8df..c4f7ef39e4 100644 --- a/pkg/sdk/identifier_helpers.go +++ b/pkg/sdk/identifier_helpers.go @@ -320,8 +320,7 @@ func NewSchemaObjectIdentifierWithArgumentsFromFullyQualifiedName(fullyQualified if err != nil { return SchemaObjectIdentifierWithArguments{}, err } - arguments := fullyQualifiedName[splitIdIndex:] - dataTypes, err := parseFunctionArgumentsFromString(arguments[1 : len(arguments)-1]) + dataTypes, err := ParseFunctionArgumentsFromString(fullyQualifiedName[splitIdIndex:]) if err != nil { return SchemaObjectIdentifierWithArguments{}, err } diff --git a/pkg/sdk/identifier_parsers.go b/pkg/sdk/identifier_parsers.go index 584761b0be..3fc4904907 100644 --- a/pkg/sdk/identifier_parsers.go +++ b/pkg/sdk/identifier_parsers.go @@ -1,6 +1,7 @@ package sdk import ( + "bytes" "encoding/csv" "fmt" "strings" @@ -37,6 +38,10 @@ func ParseIdentifierString(identifier string) ([]string, error) { if strings.Contains(part, `"`) { return nil, fmt.Errorf(`unable to parse identifier: %s, currently identifiers containing double quotes are not supported in the provider`, identifier) } + // TODO(SNOW-1571674): Remove the validation + if strings.ContainsAny(part, `()`) { + return nil, fmt.Errorf(`unable to parse identifier: %s, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`, identifier) + } } return parts, nil } @@ -140,3 +145,50 @@ func ParseExternalObjectIdentifier(identifier string) (ExternalObjectIdentifier, }, ) } + +// ParseFunctionArgumentsFromString parses function argument from arguments string with optional argument names. +// Varying types are not supported (e.g. VARCHAR(200)), because Snowflake outputs them in shortened version +// (VARCHAR in this case). The only exception is newly added type VECTOR which has the following structure +// VECTOR(, n) where right now can be either INT or FLOAT and n is the number of elements in the VECTOR. +// Snowflake returns vectors with their exact type and this function supports it. +func ParseFunctionArgumentsFromString(arguments string) ([]DataType, error) { + dataTypes := make([]DataType, 0) + + if len(arguments) > 0 && arguments[0] == '(' && arguments[len(arguments)-1] == ')' { + arguments = arguments[1 : len(arguments)-1] + } + stringBuffer := bytes.NewBufferString(arguments) + + for stringBuffer.Len() > 0 { + // We use another buffer to peek into next data type (needed for vector parsing) + peekDataType, _ := bytes.NewBufferString(stringBuffer.String()).ReadString(',') + peekDataType = strings.TrimSpace(peekDataType) + + switch { + // For now, only vectors need special parsing behavior + case strings.HasPrefix(peekDataType, "VECTOR"): + vectorDataType, _ := stringBuffer.ReadString(')') + vectorDataType = strings.TrimSpace(vectorDataType) + + if stringBuffer.Len() > 0 { + commaByte, err := stringBuffer.ReadByte() + if commaByte != ',' { + return nil, fmt.Errorf("expected a comma delimited string but found %s", string(commaByte)) + } + if err != nil { + return nil, err + } + } + dataTypes = append(dataTypes, DataType(vectorDataType)) + default: + dataType, err := stringBuffer.ReadString(',') + if err == nil { + dataType = dataType[:len(dataType)-1] + } + dataType = strings.TrimSpace(dataType) + dataTypes = append(dataTypes, DataType(dataType)) + } + } + + return dataTypes, nil +} diff --git a/pkg/sdk/identifier_parsers_test.go b/pkg/sdk/identifier_parsers_test.go index de86bbc9dc..cecb87db90 100644 --- a/pkg/sdk/identifier_parsers_test.go +++ b/pkg/sdk/identifier_parsers_test.go @@ -80,6 +80,27 @@ func Test_ParseIdentifierString(t *testing.T) { require.ErrorContains(t, err, `unable to parse identifier: "ab""c".def, currently identifiers containing double quotes are not supported in the provider`) }) + t.Run("returns error when identifier contains opening parenthesis", func(t *testing.T) { + input := `"ab(c".def` + _, err := ParseIdentifierString(input) + + require.ErrorContains(t, err, `unable to parse identifier: "ab(c".def, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`) + }) + + t.Run("returns error when identifier contains closing parenthesis", func(t *testing.T) { + input := `"ab)c".def` + _, err := ParseIdentifierString(input) + + require.ErrorContains(t, err, `unable to parse identifier: "ab)c".def, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`) + }) + + t.Run("returns error when identifier contains opening and closing parentheses", func(t *testing.T) { + input := `"ab()c".def` + _, err := ParseIdentifierString(input) + + require.ErrorContains(t, err, `unable to parse identifier: "ab()c".def, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`) + }) + t.Run("returns parts correctly with dots inside", func(t *testing.T) { input := `"ab.c".def` expected := []string{`ab.c`, "def"} @@ -250,3 +271,31 @@ func Test_ParseObjectIdentifierString(t *testing.T) { }) } } + +func Test_ParseFunctionArgumentsFromString(t *testing.T) { + testCases := []struct { + Arguments string + Expected []DataType + Error string + }{ + {Arguments: `()`, Expected: []DataType{}}, + {Arguments: `(FLOAT, NUMBER, TIME)`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataTypeTime}}, + {Arguments: `FLOAT, NUMBER, TIME`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataTypeTime}}, + {Arguments: `(FLOAT, NUMBER, VECTOR(FLOAT, 20))`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataType("VECTOR(FLOAT, 20)")}}, + {Arguments: `FLOAT, NUMBER, VECTOR(FLOAT, 20)`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataType("VECTOR(FLOAT, 20)")}}, + {Arguments: `(VECTOR(FLOAT, 10), NUMBER, VECTOR(FLOAT, 20))`, Expected: []DataType{DataType("VECTOR(FLOAT, 10)"), DataTypeNumber, DataType("VECTOR(FLOAT, 20)")}}, + {Arguments: `VECTOR(FLOAT, 10)| NUMBER, VECTOR(FLOAT, 20)`, Error: "expected a comma delimited string but found |"}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("parsing function arguments %s", testCase.Arguments), func(t *testing.T) { + dataTypes, err := ParseFunctionArgumentsFromString(testCase.Arguments) + if testCase.Error != "" { + assert.ErrorContains(t, err, testCase.Error) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.Expected, dataTypes) + } + }) + } +} diff --git a/pkg/sdk/testint/functions_integration_test.go b/pkg/sdk/testint/functions_integration_test.go index a1d57a79ee..883a90c6bd 100644 --- a/pkg/sdk/testint/functions_integration_test.go +++ b/pkg/sdk/testint/functions_integration_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections" + "github.com/stretchr/testify/assert" "testing" "time" @@ -183,268 +185,268 @@ def dump(i): }) } -//func TestInt_OtherFunctions(t *testing.T) { -// client := testClient(t) -// ctx := testContext(t) -// -// tagTest, tagCleanup := testClientHelper().Tag.CreateTag(t) -// t.Cleanup(tagCleanup) -// -// assertFunction := func(t *testing.T, id sdk.SchemaObjectIdentifierWithArguments, secure bool, withArguments bool) { -// t.Helper() -// -// function, err := client.Functions.ShowByID(ctx, id) -// require.NoError(t, err) -// -// assert.NotEmpty(t, function.CreatedOn) -// assert.Equal(t, id.Name(), function.Name) -// assert.Equal(t, false, function.IsBuiltin) -// assert.Equal(t, false, function.IsAggregate) -// assert.Equal(t, false, function.IsAnsi) -// if withArguments { -// assert.Equal(t, 1, function.MinNumArguments) -// assert.Equal(t, 1, function.MaxNumArguments) -// } else { -// assert.Equal(t, 0, function.MinNumArguments) -// assert.Equal(t, 0, function.MaxNumArguments) -// } -// assert.NotEmpty(t, function.ArgumentsRaw) -// assert.NotEmpty(t, function.Arguments) -// assert.NotEmpty(t, function.Description) -// assert.NotEmpty(t, function.CatalogName) -// assert.Equal(t, false, function.IsTableFunction) -// assert.Equal(t, false, function.ValidForClustering) -// assert.Equal(t, secure, function.IsSecure) -// assert.Equal(t, false, function.IsExternalFunction) -// assert.Equal(t, "SQL", function.Language) -// assert.Equal(t, false, function.IsMemoizable) -// } -// -// cleanupFunctionHandle := func(id sdk.SchemaObjectIdentifierWithArguments) func() { -// return func() { -// err := client.Functions.Drop(ctx, sdk.NewDropFunctionRequest(id)) -// if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { -// return -// } -// require.NoError(t, err) -// } -// } -// -// createFunctionForSQLHandle := func(t *testing.T, cleanup bool, withArguments bool) *sdk.Function { -// t.Helper() -// var id sdk.SchemaObjectIdentifierWithArguments -// if withArguments { -// id = testClientHelper().Ids.RandomSchemaObjectIdentifierWithArguments(sdk.DataTypeFloat) -// } else { -// id = testClientHelper().Ids.RandomSchemaObjectIdentifierWithArguments() -// } -// -// definition := "3.141592654::FLOAT" -// -// dt := sdk.NewFunctionReturnsResultDataTypeRequest(sdk.DataTypeFloat) -// returns := sdk.NewFunctionReturnsRequest().WithResultDataType(*dt) -// request := sdk.NewCreateForSQLFunctionRequest(id.SchemaObjectId(), *returns, definition). -// WithOrReplace(true) -// if withArguments { -// argument := sdk.NewFunctionArgumentRequest("x", sdk.DataTypeFloat) -// request = request.WithArguments([]sdk.FunctionArgumentRequest{*argument}) -// } -// err := client.Functions.CreateForSQL(ctx, request) -// require.NoError(t, err) -// if cleanup { -// t.Cleanup(cleanupFunctionHandle(id)) -// } -// function, err := client.Functions.ShowByID(ctx, id) -// require.NoError(t, err) -// return function -// } -// -// defaultAlterRequest := func(id sdk.SchemaObjectIdentifierWithArguments) *sdk.AlterFunctionRequest { -// return sdk.NewAlterFunctionRequest(id) -// } -// -// t.Run("alter function: rename", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, false, true) -// -// id := f.ID() -// nid := testClientHelper().Ids.RandomSchemaObjectIdentifierWithArguments() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithRenameTo(nid.SchemaObjectId())) -// if err != nil { -// t.Cleanup(cleanupFunctionHandle(id)) -// } else { -// t.Cleanup(cleanupFunctionHandle(nid)) -// } -// require.NoError(t, err) -// -// _, err = client.Functions.ShowByID(ctx, id) -// assert.ErrorIs(t, err, collections.ErrObjectNotFound) -// -// e, err := client.Functions.ShowByID(ctx, nid) -// require.NoError(t, err) -// require.Equal(t, nid.Name(), e.Name) -// }) -// -// t.Run("alter function: set log level", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetLogLevel(string(sdk.LogLevelDebug))) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: unset log level", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetLogLevel(true)) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: set trace level", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetTraceLevel(string(sdk.TraceLevelAlways))) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: unset trace level", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetTraceLevel(true)) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: set comment", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetComment("test comment")) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: unset comment", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetComment(true)) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: set secure", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetSecure(true)) -// require.NoError(t, err) -// assertFunction(t, id, true, true) -// }) -// -// t.Run("alter function: set secure with no arguments", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// id := f.ID() -// err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSetSecure(true)) -// require.NoError(t, err) -// assertFunction(t, id, true, true) -// }) -// -// t.Run("alter function: unset secure", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetSecure(true)) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("alter function: set and unset tags", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// id := f.ID() -// setTags := []sdk.TagAssociation{ -// { -// Name: tagTest.ID(), -// Value: "v1", -// }, -// } -// err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetTags(setTags)) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// -// unsetTags := []sdk.ObjectIdentifier{ -// tagTest.ID(), -// } -// err = client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetTags(unsetTags)) -// require.NoError(t, err) -// assertFunction(t, id, false, true) -// }) -// -// t.Run("show function for SQL: without like", func(t *testing.T) { -// f1 := createFunctionForSQLHandle(t, true, true) -// f2 := createFunctionForSQLHandle(t, true, true) -// -// functions, err := client.Functions.Show(ctx, sdk.NewShowFunctionRequest()) -// require.NoError(t, err) -// -// require.Contains(t, functions, *f1) -// require.Contains(t, functions, *f2) -// }) -// -// t.Run("show function for SQL: with like", func(t *testing.T) { -// f1 := createFunctionForSQLHandle(t, true, true) -// f2 := createFunctionForSQLHandle(t, true, true) -// -// functions, err := client.Functions.Show(ctx, sdk.NewShowFunctionRequest().WithLike(sdk.Like{Pattern: &f1.Name})) -// require.NoError(t, err) -// -// require.Equal(t, 1, len(functions)) -// require.Contains(t, functions, *f1) -// require.NotContains(t, functions, *f2) -// }) -// -// t.Run("show function for SQL: no matches", func(t *testing.T) { -// functions, err := client.Functions.Show(ctx, sdk.NewShowFunctionRequest().WithLike(sdk.Like{Pattern: sdk.String("non-existing-id-pattern")})) -// require.NoError(t, err) -// require.Equal(t, 0, len(functions)) -// }) -// -// t.Run("describe function for SQL", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, true) -// -// details, err := client.Functions.Describe(ctx, f.ID()) -// require.NoError(t, err) -// pairs := make(map[string]string) -// for _, detail := range details { -// pairs[detail.Property] = detail.Value -// } -// require.Equal(t, "SQL", pairs["language"]) -// require.Equal(t, "FLOAT", pairs["returns"]) -// require.Equal(t, "3.141592654::FLOAT", pairs["body"]) -// require.Equal(t, "(X FLOAT)", pairs["signature"]) -// }) -// -// t.Run("describe function for SQL: no arguments", func(t *testing.T) { -// f := createFunctionForSQLHandle(t, true, false) -// -// details, err := client.Functions.Describe(ctx, f.ID()) -// require.NoError(t, err) -// pairs := make(map[string]string) -// for _, detail := range details { -// pairs[detail.Property] = detail.Value -// } -// require.Equal(t, "SQL", pairs["language"]) -// require.Equal(t, "FLOAT", pairs["returns"]) -// require.Equal(t, "3.141592654::FLOAT", pairs["body"]) -// require.Equal(t, "()", pairs["signature"]) -// }) -//} +func TestInt_OtherFunctions(t *testing.T) { + client := testClient(t) + ctx := testContext(t) + + tagTest, tagCleanup := testClientHelper().Tag.CreateTag(t) + t.Cleanup(tagCleanup) + + assertFunction := func(t *testing.T, id sdk.SchemaObjectIdentifierWithArguments, secure bool, withArguments bool) { + t.Helper() + + function, err := client.Functions.ShowByID(ctx, id) + require.NoError(t, err) + + assert.NotEmpty(t, function.CreatedOn) + assert.Equal(t, id.Name(), function.Name) + assert.Equal(t, false, function.IsBuiltin) + assert.Equal(t, false, function.IsAggregate) + assert.Equal(t, false, function.IsAnsi) + if withArguments { + assert.Equal(t, 1, function.MinNumArguments) + assert.Equal(t, 1, function.MaxNumArguments) + } else { + assert.Equal(t, 0, function.MinNumArguments) + assert.Equal(t, 0, function.MaxNumArguments) + } + assert.NotEmpty(t, function.ArgumentsRaw) + assert.NotEmpty(t, function.Arguments) + assert.NotEmpty(t, function.Description) + assert.NotEmpty(t, function.CatalogName) + assert.Equal(t, false, function.IsTableFunction) + assert.Equal(t, false, function.ValidForClustering) + assert.Equal(t, secure, function.IsSecure) + assert.Equal(t, false, function.IsExternalFunction) + assert.Equal(t, "SQL", function.Language) + assert.Equal(t, false, function.IsMemoizable) + } + + cleanupFunctionHandle := func(id sdk.SchemaObjectIdentifierWithArguments) func() { + return func() { + err := client.Functions.Drop(ctx, sdk.NewDropFunctionRequest(id)) + if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) { + return + } + require.NoError(t, err) + } + } + + createFunctionForSQLHandle := func(t *testing.T, cleanup bool, withArguments bool) *sdk.Function { + t.Helper() + var id sdk.SchemaObjectIdentifierWithArguments + if withArguments { + id = testClientHelper().Ids.RandomSchemaObjectIdentifierWithArguments(sdk.DataTypeFloat) + } else { + id = testClientHelper().Ids.RandomSchemaObjectIdentifierWithArguments() + } + + definition := "3.141592654::FLOAT" + + dt := sdk.NewFunctionReturnsResultDataTypeRequest(sdk.DataTypeFloat) + returns := sdk.NewFunctionReturnsRequest().WithResultDataType(*dt) + request := sdk.NewCreateForSQLFunctionRequest(id.SchemaObjectId(), *returns, definition). + WithOrReplace(true) + if withArguments { + argument := sdk.NewFunctionArgumentRequest("x", sdk.DataTypeFloat) + request = request.WithArguments([]sdk.FunctionArgumentRequest{*argument}) + } + err := client.Functions.CreateForSQL(ctx, request) + require.NoError(t, err) + if cleanup { + t.Cleanup(cleanupFunctionHandle(id)) + } + function, err := client.Functions.ShowByID(ctx, id) + require.NoError(t, err) + return function + } + + defaultAlterRequest := func(id sdk.SchemaObjectIdentifierWithArguments) *sdk.AlterFunctionRequest { + return sdk.NewAlterFunctionRequest(id) + } + + t.Run("alter function: rename", func(t *testing.T) { + f := createFunctionForSQLHandle(t, false, true) + + id := f.ID() + nid := testClientHelper().Ids.RandomSchemaObjectIdentifierWithArguments() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithRenameTo(nid.SchemaObjectId())) + if err != nil { + t.Cleanup(cleanupFunctionHandle(id)) + } else { + t.Cleanup(cleanupFunctionHandle(nid)) + } + require.NoError(t, err) + + _, err = client.Functions.ShowByID(ctx, id) + assert.ErrorIs(t, err, collections.ErrObjectNotFound) + + e, err := client.Functions.ShowByID(ctx, nid) + require.NoError(t, err) + require.Equal(t, nid.Name(), e.Name) + }) + + t.Run("alter function: set log level", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetLogLevel(string(sdk.LogLevelDebug))) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: unset log level", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetLogLevel(true)) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: set trace level", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetTraceLevel(string(sdk.TraceLevelAlways))) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: unset trace level", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetTraceLevel(true)) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: set comment", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetComment("test comment")) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: unset comment", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetComment(true)) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: set secure", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetSecure(true)) + require.NoError(t, err) + assertFunction(t, id, true, true) + }) + + t.Run("alter function: set secure with no arguments", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + id := f.ID() + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSetSecure(true)) + require.NoError(t, err) + assertFunction(t, id, true, true) + }) + + t.Run("alter function: unset secure", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetSecure(true)) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("alter function: set and unset tags", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + id := f.ID() + setTags := []sdk.TagAssociation{ + { + Name: tagTest.ID(), + Value: "v1", + }, + } + err := client.Functions.Alter(ctx, defaultAlterRequest(id).WithSetTags(setTags)) + require.NoError(t, err) + assertFunction(t, id, false, true) + + unsetTags := []sdk.ObjectIdentifier{ + tagTest.ID(), + } + err = client.Functions.Alter(ctx, defaultAlterRequest(id).WithUnsetTags(unsetTags)) + require.NoError(t, err) + assertFunction(t, id, false, true) + }) + + t.Run("show function for SQL: without like", func(t *testing.T) { + f1 := createFunctionForSQLHandle(t, true, true) + f2 := createFunctionForSQLHandle(t, true, true) + + functions, err := client.Functions.Show(ctx, sdk.NewShowFunctionRequest()) + require.NoError(t, err) + + require.Contains(t, functions, *f1) + require.Contains(t, functions, *f2) + }) + + t.Run("show function for SQL: with like", func(t *testing.T) { + f1 := createFunctionForSQLHandle(t, true, true) + f2 := createFunctionForSQLHandle(t, true, true) + + functions, err := client.Functions.Show(ctx, sdk.NewShowFunctionRequest().WithLike(sdk.Like{Pattern: &f1.Name})) + require.NoError(t, err) + + require.Equal(t, 1, len(functions)) + require.Contains(t, functions, *f1) + require.NotContains(t, functions, *f2) + }) + + t.Run("show function for SQL: no matches", func(t *testing.T) { + functions, err := client.Functions.Show(ctx, sdk.NewShowFunctionRequest().WithLike(sdk.Like{Pattern: sdk.String("non-existing-id-pattern")})) + require.NoError(t, err) + require.Equal(t, 0, len(functions)) + }) + + t.Run("describe function for SQL", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, true) + + details, err := client.Functions.Describe(ctx, f.ID()) + require.NoError(t, err) + pairs := make(map[string]string) + for _, detail := range details { + pairs[detail.Property] = detail.Value + } + require.Equal(t, "SQL", pairs["language"]) + require.Equal(t, "FLOAT", pairs["returns"]) + require.Equal(t, "3.141592654::FLOAT", pairs["body"]) + require.Equal(t, "(X FLOAT)", pairs["signature"]) + }) + + t.Run("describe function for SQL: no arguments", func(t *testing.T) { + f := createFunctionForSQLHandle(t, true, false) + + details, err := client.Functions.Describe(ctx, f.ID()) + require.NoError(t, err) + pairs := make(map[string]string) + for _, detail := range details { + pairs[detail.Property] = detail.Value + } + require.Equal(t, "SQL", pairs["language"]) + require.Equal(t, "FLOAT", pairs["returns"]) + require.Equal(t, "3.141592654::FLOAT", pairs["body"]) + require.Equal(t, "()", pairs["signature"]) + }) +} func TestInt_FunctionsShowByID(t *testing.T) { client := testClient(t) @@ -488,20 +490,14 @@ func TestInt_FunctionsShowByID(t *testing.T) { e1, err := client.Functions.ShowByID(ctx, id1) require.NoError(t, err) - e1Details, err := client.Functions.Describe(ctx, id1) - require.NoError(t, err) - - e1Id, err := e1.ID(e1Details) + e1Id := e1.ID() require.NoError(t, err) require.Equal(t, id1, e1Id) e2, err := client.Functions.ShowByID(ctx, id2) require.NoError(t, err) - e2Details, err := client.Functions.Describe(ctx, id2) - require.NoError(t, err) - - e2Id, err := e2.ID(e2Details) + e2Id := e2.ID() require.NoError(t, err) require.Equal(t, id2, e2Id) }) @@ -560,4 +556,60 @@ func TestInt_FunctionsShowByID(t *testing.T) { _, err = client.Functions.ShowByID(ctx, idWithArguments) require.NoError(t, err) }) + + t.Run("function returns non detailed data types of arguments", func(t *testing.T) { + // This test proves that every detailed data type (e.g. VARCHAR(20) and NUMBER(10, 0)) is generalized + // (to e.g. VARCHAR and NUMBER) and that sdk.ToDataType mapping function maps detailed types correctly to + // their generalized counterparts. + + id := testClientHelper().Ids.RandomSchemaObjectIdentifier() + args := []sdk.FunctionArgumentRequest{ + *sdk.NewFunctionArgumentRequest("A", "NUMBER(2, 0)"), + *sdk.NewFunctionArgumentRequest("B", "DECIMAL"), + *sdk.NewFunctionArgumentRequest("C", "INTEGER"), + *sdk.NewFunctionArgumentRequest("D", sdk.DataTypeFloat), + *sdk.NewFunctionArgumentRequest("E", "DOUBLE"), + *sdk.NewFunctionArgumentRequest("F", "VARCHAR(20)"), + *sdk.NewFunctionArgumentRequest("G", "CHAR"), + *sdk.NewFunctionArgumentRequest("H", sdk.DataTypeString), + *sdk.NewFunctionArgumentRequest("I", "TEXT"), + *sdk.NewFunctionArgumentRequest("J", sdk.DataTypeBinary), + *sdk.NewFunctionArgumentRequest("K", "VARBINARY"), + *sdk.NewFunctionArgumentRequest("L", sdk.DataTypeBoolean), + *sdk.NewFunctionArgumentRequest("M", sdk.DataTypeDate), + *sdk.NewFunctionArgumentRequest("N", "DATETIME"), + *sdk.NewFunctionArgumentRequest("O", sdk.DataTypeTime), + *sdk.NewFunctionArgumentRequest("P", sdk.DataTypeTimestamp), + *sdk.NewFunctionArgumentRequest("R", sdk.DataTypeTimestampLTZ), + *sdk.NewFunctionArgumentRequest("S", sdk.DataTypeTimestampNTZ), + *sdk.NewFunctionArgumentRequest("T", sdk.DataTypeTimestampTZ), + *sdk.NewFunctionArgumentRequest("U", sdk.DataTypeVariant), + *sdk.NewFunctionArgumentRequest("V", sdk.DataTypeObject), + *sdk.NewFunctionArgumentRequest("W", sdk.DataTypeArray), + *sdk.NewFunctionArgumentRequest("X", sdk.DataTypeGeography), + *sdk.NewFunctionArgumentRequest("Y", sdk.DataTypeGeometry), + *sdk.NewFunctionArgumentRequest("Z", "VECTOR(INT, 16)"), + } + err := client.Functions.CreateForPython(ctx, sdk.NewCreateForPythonFunctionRequest( + id, + *sdk.NewFunctionReturnsRequest().WithResultDataType(*sdk.NewFunctionReturnsResultDataTypeRequest(sdk.DataTypeVariant)), + "3.8", + "add", + ). + WithArguments(args). + WithFunctionDefinition("def add(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, R, S, T, U, V, W, X, Y, Z): A + A"), + ) + require.NoError(t, err) + + dataTypes := make([]sdk.DataType, len(args)) + for i, arg := range args { + dataTypes[i], err = sdk.ToDataType(string(arg.ArgDataType)) + require.NoError(t, err) + } + idWithArguments := sdk.NewSchemaObjectIdentifierWithArguments(id.DatabaseName(), id.SchemaName(), id.Name(), dataTypes...) + + function, err := client.Functions.ShowByID(ctx, idWithArguments) + require.NoError(t, err) + require.Equal(t, dataTypes, function.Arguments) + }) }