diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index f5c8f2d76ad..edb760e4126 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -436,62 +436,62 @@ func getResources() map[string]*schema.Resource { "snowflake_database_role": resources.DatabaseRole(), "snowflake_dynamic_table": resources.DynamicTable(), "snowflake_email_notification_integration": resources.EmailNotificationIntegration(), - //"snowflake_external_function": resources.ExternalFunction(), - "snowflake_external_oauth_integration": resources.ExternalOauthIntegration(), - "snowflake_external_table": resources.ExternalTable(), - "snowflake_failover_group": resources.FailoverGroup(), - "snowflake_file_format": resources.FileFormat(), - "snowflake_function": resources.Function(), - "snowflake_grant_account_role": resources.GrantAccountRole(), - "snowflake_grant_application_role": resources.GrantApplicationRole(), - "snowflake_grant_database_role": resources.GrantDatabaseRole(), - "snowflake_grant_ownership": resources.GrantOwnership(), - "snowflake_grant_privileges_to_account_role": resources.GrantPrivilegesToAccountRole(), - "snowflake_grant_privileges_to_database_role": resources.GrantPrivilegesToDatabaseRole(), - "snowflake_grant_privileges_to_share": resources.GrantPrivilegesToShare(), - "snowflake_managed_account": resources.ManagedAccount(), - "snowflake_masking_policy": resources.MaskingPolicy(), - "snowflake_materialized_view": resources.MaterializedView(), - "snowflake_network_policy": resources.NetworkPolicy(), - "snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(), - "snowflake_network_rule": resources.NetworkRule(), - "snowflake_notification_integration": resources.NotificationIntegration(), - "snowflake_oauth_integration": resources.OAuthIntegration(), - "snowflake_oauth_integration_for_partner_applications": resources.OauthIntegrationForPartnerApplications(), - "snowflake_oauth_integration_for_custom_clients": resources.OauthIntegrationForCustomClients(), - "snowflake_object_parameter": resources.ObjectParameter(), - "snowflake_password_policy": resources.PasswordPolicy(), - "snowflake_pipe": resources.Pipe(), - //"snowflake_procedure": resources.Procedure(), - "snowflake_resource_monitor": resources.ResourceMonitor(), - "snowflake_role": resources.Role(), - "snowflake_row_access_policy": resources.RowAccessPolicy(), - "snowflake_saml_integration": resources.SAMLIntegration(), - "snowflake_saml2_integration": resources.SAML2Integration(), - "snowflake_schema": resources.Schema(), - "snowflake_scim_integration": resources.SCIMIntegration(), - "snowflake_secondary_database": resources.SecondaryDatabase(), - "snowflake_sequence": resources.Sequence(), - "snowflake_session_parameter": resources.SessionParameter(), - "snowflake_share": resources.Share(), - "snowflake_shared_database": resources.SharedDatabase(), - "snowflake_stage": resources.Stage(), - "snowflake_storage_integration": resources.StorageIntegration(), - "snowflake_stream": resources.Stream(), - "snowflake_streamlit": resources.Streamlit(), - "snowflake_table": resources.Table(), - "snowflake_table_column_masking_policy_application": resources.TableColumnMaskingPolicyApplication(), - "snowflake_table_constraint": resources.TableConstraint(), - "snowflake_tag": resources.Tag(), - "snowflake_tag_association": resources.TagAssociation(), - "snowflake_tag_masking_policy_association": resources.TagMaskingPolicyAssociation(), - "snowflake_task": resources.Task(), - "snowflake_unsafe_execute": resources.UnsafeExecute(), - "snowflake_user": resources.User(), - "snowflake_user_password_policy_attachment": resources.UserPasswordPolicyAttachment(), - "snowflake_user_public_keys": resources.UserPublicKeys(), - "snowflake_view": resources.View(), - "snowflake_warehouse": resources.Warehouse(), + "snowflake_external_function": resources.ExternalFunction(), + "snowflake_external_oauth_integration": resources.ExternalOauthIntegration(), + "snowflake_external_table": resources.ExternalTable(), + "snowflake_failover_group": resources.FailoverGroup(), + "snowflake_file_format": resources.FileFormat(), + "snowflake_function": resources.Function(), + "snowflake_grant_account_role": resources.GrantAccountRole(), + "snowflake_grant_application_role": resources.GrantApplicationRole(), + "snowflake_grant_database_role": resources.GrantDatabaseRole(), + "snowflake_grant_ownership": resources.GrantOwnership(), + "snowflake_grant_privileges_to_account_role": resources.GrantPrivilegesToAccountRole(), + "snowflake_grant_privileges_to_database_role": resources.GrantPrivilegesToDatabaseRole(), + "snowflake_grant_privileges_to_share": resources.GrantPrivilegesToShare(), + "snowflake_managed_account": resources.ManagedAccount(), + "snowflake_masking_policy": resources.MaskingPolicy(), + "snowflake_materialized_view": resources.MaterializedView(), + "snowflake_network_policy": resources.NetworkPolicy(), + "snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(), + "snowflake_network_rule": resources.NetworkRule(), + "snowflake_notification_integration": resources.NotificationIntegration(), + "snowflake_oauth_integration": resources.OAuthIntegration(), + "snowflake_oauth_integration_for_partner_applications": resources.OauthIntegrationForPartnerApplications(), + "snowflake_oauth_integration_for_custom_clients": resources.OauthIntegrationForCustomClients(), + "snowflake_object_parameter": resources.ObjectParameter(), + "snowflake_password_policy": resources.PasswordPolicy(), + "snowflake_pipe": resources.Pipe(), + "snowflake_procedure": resources.Procedure(), + "snowflake_resource_monitor": resources.ResourceMonitor(), + "snowflake_role": resources.Role(), + "snowflake_row_access_policy": resources.RowAccessPolicy(), + "snowflake_saml_integration": resources.SAMLIntegration(), + "snowflake_saml2_integration": resources.SAML2Integration(), + "snowflake_schema": resources.Schema(), + "snowflake_scim_integration": resources.SCIMIntegration(), + "snowflake_secondary_database": resources.SecondaryDatabase(), + "snowflake_sequence": resources.Sequence(), + "snowflake_session_parameter": resources.SessionParameter(), + "snowflake_share": resources.Share(), + "snowflake_shared_database": resources.SharedDatabase(), + "snowflake_stage": resources.Stage(), + "snowflake_storage_integration": resources.StorageIntegration(), + "snowflake_stream": resources.Stream(), + "snowflake_streamlit": resources.Streamlit(), + "snowflake_table": resources.Table(), + "snowflake_table_column_masking_policy_application": resources.TableColumnMaskingPolicyApplication(), + "snowflake_table_constraint": resources.TableConstraint(), + "snowflake_tag": resources.Tag(), + "snowflake_tag_association": resources.TagAssociation(), + "snowflake_tag_masking_policy_association": resources.TagMaskingPolicyAssociation(), + "snowflake_task": resources.Task(), + "snowflake_unsafe_execute": resources.UnsafeExecute(), + "snowflake_user": resources.User(), + "snowflake_user_password_policy_attachment": resources.UserPasswordPolicyAttachment(), + "snowflake_user_public_keys": resources.UserPublicKeys(), + "snowflake_view": resources.View(), + "snowflake_warehouse": resources.Warehouse(), } } diff --git a/pkg/resources/external_function.go b/pkg/resources/external_function.go index a78e9c5d6b6..7fb246ab337 100644 --- a/pkg/resources/external_function.go +++ b/pkg/resources/external_function.go @@ -1,489 +1,504 @@ package resources -//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 -//} +import ( + "context" + "encoding/json" + "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" + "log" + "regexp" + "strconv" + "strings" +) + +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.NewSchemaObjectIdentifierWithArgumentsOld(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(sdk.NewSchemaObjectIdentifierWithArguments(id.DatabaseName(), id.SchemaName(), id.Name(), 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(sdk.NewSchemaObjectIdentifierWithArguments(id.DatabaseName(), id.SchemaName(), id.Name(), 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_state_upgraders.go b/pkg/resources/external_function_state_upgraders.go index d27a7327001..ebfd6fe2a46 100644 --- a/pkg/resources/external_function_state_upgraders.go +++ b/pkg/resources/external_function_state_upgraders.go @@ -1,69 +1,76 @@ package resources -//type v085ExternalFunctionId struct { -// DatabaseName string -// SchemaName string -// ExternalFunctionName string -// ExternalFunctionArgTypes string -//} -// -//func parseV085ExternalFunctionId(stringID string) (*v085ExternalFunctionId, error) { -// reader := csv.NewReader(strings.NewReader(stringID)) -// reader.Comma = '|' -// lines, err := reader.ReadAll() -// if err != nil { -// return nil, sdk.NewError("not CSV compatible") -// } -// -// if len(lines) != 1 { -// return nil, sdk.NewError("1 line at a time") -// } -// if len(lines[0]) != 4 { -// return nil, sdk.NewError("4 fields allowed") -// } -// -// return &v085ExternalFunctionId{ -// DatabaseName: lines[0][0], -// SchemaName: lines[0][1], -// ExternalFunctionName: lines[0][2], -// ExternalFunctionArgTypes: lines[0][3], -// }, nil -//} -// -//func v085ExternalFunctionStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { -// if rawState == nil { -// return rawState, nil -// } -// -// oldId := rawState["id"].(string) -// parsedV085ExternalFunctionId, err := parseV085ExternalFunctionId(oldId) -// if err != nil { -// return nil, err -// } -// -// argDataTypes := make([]sdk.DataType, 0) -// if parsedV085ExternalFunctionId.ExternalFunctionArgTypes != "" { -// for _, argType := range strings.Split(parsedV085ExternalFunctionId.ExternalFunctionArgTypes, "-") { -// argDataType, err := sdk.ToDataType(argType) -// if err != nil { -// return nil, err -// } -// argDataTypes = append(argDataTypes, argDataType) -// } -// } -// -// schemaObjectIdentifierWithArguments := sdk.NewSchemaObjectIdentifierWithArguments(parsedV085ExternalFunctionId.DatabaseName, parsedV085ExternalFunctionId.SchemaName, parsedV085ExternalFunctionId.ExternalFunctionName, argDataTypes) -// rawState["id"] = schemaObjectIdentifierWithArguments.FullyQualifiedName() -// -// oldDatabase := rawState["database"].(string) -// oldSchema := rawState["schema"].(string) -// -// rawState["database"] = strings.Trim(oldDatabase, "\"") -// rawState["schema"] = strings.Trim(oldSchema, "\"") -// -// if old, isPresent := rawState["return_null_allowed"]; !isPresent || old == nil { -// rawState["return_null_allowed"] = "true" -// } -// -// return rawState, nil -//} +import ( + "context" + "encoding/csv" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "strings" +) + +type v085ExternalFunctionId struct { + DatabaseName string + SchemaName string + ExternalFunctionName string + ExternalFunctionArgTypes string +} + +func parseV085ExternalFunctionId(stringID string) (*v085ExternalFunctionId, error) { + reader := csv.NewReader(strings.NewReader(stringID)) + reader.Comma = '|' + lines, err := reader.ReadAll() + if err != nil { + return nil, sdk.NewError("not CSV compatible") + } + + if len(lines) != 1 { + return nil, sdk.NewError("1 line at a time") + } + if len(lines[0]) != 4 { + return nil, sdk.NewError("4 fields allowed") + } + + return &v085ExternalFunctionId{ + DatabaseName: lines[0][0], + SchemaName: lines[0][1], + ExternalFunctionName: lines[0][2], + ExternalFunctionArgTypes: lines[0][3], + }, nil +} + +func v085ExternalFunctionStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + if rawState == nil { + return rawState, nil + } + + oldId := rawState["id"].(string) + parsedV085ExternalFunctionId, err := parseV085ExternalFunctionId(oldId) + if err != nil { + return nil, err + } + + argDataTypes := make([]sdk.DataType, 0) + if parsedV085ExternalFunctionId.ExternalFunctionArgTypes != "" { + for _, argType := range strings.Split(parsedV085ExternalFunctionId.ExternalFunctionArgTypes, "-") { + argDataType, err := sdk.ToDataType(argType) + if err != nil { + return nil, err + } + argDataTypes = append(argDataTypes, argDataType) + } + } + + schemaObjectIdentifierWithArguments := sdk.NewSchemaObjectIdentifierWithArgumentsOld(parsedV085ExternalFunctionId.DatabaseName, parsedV085ExternalFunctionId.SchemaName, parsedV085ExternalFunctionId.ExternalFunctionName, argDataTypes) + rawState["id"] = schemaObjectIdentifierWithArguments.FullyQualifiedName() + + oldDatabase := rawState["database"].(string) + oldSchema := rawState["schema"].(string) + + rawState["database"] = strings.Trim(oldDatabase, "\"") + rawState["schema"] = strings.Trim(oldSchema, "\"") + + if old, isPresent := rawState["return_null_allowed"]; !isPresent || old == nil { + rawState["return_null_allowed"] = "true" + } + + return rawState, nil +} diff --git a/pkg/resources/function.go b/pkg/resources/function.go index 747429a2702..4b8d187f47a 100644 --- a/pkg/resources/function.go +++ b/pkg/resources/function.go @@ -48,6 +48,7 @@ var functionSchema = map[string]*schema.Schema{ }, Description: "The argument name", }, + // TODO(SNOW-1596962): Fully support VECTOR data type sdk.ParseFunctionArgumentsFromString could be a base for another function that takes argument names into consideration. "type": { Type: schema.TypeString, Required: true, diff --git a/pkg/resources/function_acceptance_test.go b/pkg/resources/function_acceptance_test.go index 2a41bff811e..a05bdd1ff6d 100644 --- a/pkg/resources/function_acceptance_test.go +++ b/pkg/resources/function_acceptance_test.go @@ -2,7 +2,6 @@ package resources_test import ( "fmt" - "regexp" "strings" "testing" @@ -237,7 +236,7 @@ func TestAcc_Function_migrateFromVersion085(t *testing.T) { }) } -func TestAcc_Function_Version0941_ResourceIdMigration(t *testing.T) { +func TestAcc_Function_Version0941_EnsureSmoothResourceIdMigration(t *testing.T) { name := acc.TestClient().Ids.RandomAccountObjectIdentifier().Name() resourceName := "snowflake_function.f" @@ -255,17 +254,16 @@ func TestAcc_Function_Version0941_ResourceIdMigration(t *testing.T) { Source: "Snowflake-Labs/snowflake", }, }, - Config: functionConfigWithVector(acc.TestDatabaseName, acc.TestSchemaName, name, ""), + Config: functionConfigWithMoreArguments(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)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%s"."%s"."%s"(VARCHAR, FLOAT, NUMBER)`, 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, ""), + Config: functionConfigWithMoreArguments(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)), + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%s"."%s"."%s"(VARCHAR, FLOAT, NUMBER)`, acc.TestDatabaseName, acc.TestSchemaName, name)), ), }, }, @@ -313,39 +311,30 @@ func TestAcc_Function_Rename(t *testing.T) { }) } -func functionConfigWithVector(database string, schema string, name string, comment string) string { +func functionConfigWithMoreArguments(database string, schema string, name 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)" + type = "VARCHAR" } 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)" + name = "C" + type = "NUMBER" } } -`, database, schema, name, comment) +`, database, schema, name) } func functionConfig(database string, schema string, name string, comment string) string { diff --git a/pkg/resources/procedure.go b/pkg/resources/procedure.go index 506b2100217..fc6e918da8b 100644 --- a/pkg/resources/procedure.go +++ b/pkg/resources/procedure.go @@ -1,779 +1,794 @@ package resources -//var procedureSchema = map[string]*schema.Schema{ -// "name": { -// Type: schema.TypeString, -// Required: true, -// Description: "Specifies the identifier for the procedure; does not have to be unique for the schema in which the procedure is created. Don't use the | character.", -// }, -// "database": { -// Type: schema.TypeString, -// Required: true, -// Description: "The database in which to create the procedure. Don't use the | character.", -// ForceNew: true, -// }, -// "schema": { -// Type: schema.TypeString, -// Required: true, -// Description: "The schema in which to create the procedure. Don't use the | character.", -// ForceNew: true, -// }, -// "secure": { -// Type: schema.TypeBool, -// Optional: true, -// Description: "Specifies that the procedure is secure. For more information about secure procedures, see Protecting Sensitive Information with Secure UDFs and Stored Procedures.", -// Default: false, -// }, -// "arguments": { -// Type: schema.TypeList, -// 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(old, new) -// }, -// Description: "The argument name", -// }, -// "type": { -// Type: schema.TypeString, -// Required: true, -// ValidateFunc: dataTypeValidateFunc, -// DiffSuppressFunc: dataTypeDiffSuppressFunc, -// Description: "The argument type", -// }, -// }, -// }, -// Optional: true, -// Description: "List of the arguments for the procedure", -// ForceNew: true, -// }, -// "return_type": { -// Type: schema.TypeString, -// Description: "The return type of the procedure", -// // 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 { -// if strings.EqualFold(old, new) { -// return true -// } -// -// varcharType := []string{"VARCHAR(16777216)", "VARCHAR", "text", "string", "NVARCHAR", "NVARCHAR2", "CHAR VARYING", "NCHAR VARYING"} -// if slices.Contains(varcharType, strings.ToUpper(old)) && slices.Contains(varcharType, strings.ToUpper(new)) { -// return true -// } -// -// // all these types are equivalent https://docs.snowflake.com/en/sql-reference/data-types-numeric.html#int-integer-bigint-smallint-tinyint-byteint -// integerTypes := []string{"INT", "INTEGER", "BIGINT", "SMALLINT", "TINYINT", "BYTEINT", "NUMBER(38,0)"} -// if slices.Contains(integerTypes, strings.ToUpper(old)) && slices.Contains(integerTypes, strings.ToUpper(new)) { -// return true -// } -// return false -// }, -// Required: true, -// ForceNew: true, -// }, -// "statement": { -// Type: schema.TypeString, -// Required: true, -// Description: "Specifies the code used to create the procedure.", -// ForceNew: true, -// DiffSuppressFunc: DiffSuppressStatement, -// }, -// "language": { -// Type: schema.TypeString, -// Optional: true, -// Default: "SQL", -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// return strings.EqualFold(old, new) -// }, -// ValidateFunc: validation.StringInSlice([]string{"javascript", "java", "scala", "SQL", "python"}, true), -// Description: "Specifies the language of the stored procedure code.", -// }, -// "execute_as": { -// Type: schema.TypeString, -// Optional: true, -// Default: "OWNER", -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// return strings.EqualFold(old, new) -// }, -// ValidateFunc: validation.StringInSlice([]string{"CALLER", "OWNER"}, true), -// Description: "Sets execution context. Allowed values are CALLER and OWNER (consult a proper section in the [docs](https://docs.snowflake.com/en/sql-reference/sql/create-procedure#id1)). For more information see [caller's rights and owner's rights](https://docs.snowflake.com/en/developer-guide/stored-procedure/stored-procedures-rights).", -// }, -// "null_input_behavior": { -// Type: schema.TypeString, -// Optional: true, -// Default: "CALLED ON NULL INPUT", -// ForceNew: true, -// // We do not use STRICT, because Snowflake then in the Read phase returns RETURNS NULL ON NULL INPUT -// ValidateFunc: validation.StringInSlice([]string{"CALLED ON NULL INPUT", "RETURNS NULL ON NULL INPUT"}, false), -// Description: "Specifies the behavior of the procedure when called with null inputs.", -// }, -// "return_behavior": { -// Type: schema.TypeString, -// Optional: true, -// Default: "VOLATILE", -// ForceNew: true, -// ValidateFunc: validation.StringInSlice([]string{"VOLATILE", "IMMUTABLE"}, false), -// Description: "Specifies the behavior of the function when returning results", -// Deprecated: "These keywords are deprecated for stored procedures. These keywords are not intended to apply to stored procedures. In a future release, these keywords will be removed from the documentation.", -// }, -// "comment": { -// Type: schema.TypeString, -// Optional: true, -// Default: "user-defined procedure", -// Description: "Specifies a comment for the procedure.", -// }, -// "runtime_version": { -// Type: schema.TypeString, -// Optional: true, -// ForceNew: true, -// Description: "Required for Python procedures. Specifies Python runtime version.", -// }, -// "packages": { -// Type: schema.TypeList, -// Elem: &schema.Schema{ -// Type: schema.TypeString, -// }, -// Optional: true, -// ForceNew: true, -// Description: "List of package imports to use for Java / Python procedures. For Java, package imports should be of the form: package_name:version_number, where package_name is snowflake_domain:package. For Python use it should be: ('numpy','pandas','xgboost==1.5.0').", -// }, -// "imports": { -// Type: schema.TypeList, -// Elem: &schema.Schema{ -// Type: schema.TypeString, -// }, -// Optional: true, -// ForceNew: true, -// Description: "Imports for Java / Python procedures. For Java this a list of jar files, for Python this is a list of Python files.", -// }, -// "handler": { -// Type: schema.TypeString, -// Optional: true, -// ForceNew: true, -// Description: "The handler method for Java / Python procedures.", -// }, -//} -// -//// Procedure returns a pointer to the resource representing a stored procedure. -//func Procedure() *schema.Resource { -// return &schema.Resource{ -// SchemaVersion: 1, -// -// CreateContext: CreateContextProcedure, -// ReadContext: ReadContextProcedure, -// UpdateContext: UpdateContextProcedure, -// DeleteContext: DeleteContextProcedure, -// -// Schema: procedureSchema, -// 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: v085ProcedureStateUpgrader, -// }, -// }, -// } -//} -// -//func CreateContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// lang := strings.ToUpper(d.Get("language").(string)) -// switch lang { -// case "JAVA": -// return createJavaProcedure(ctx, d, meta) -// case "JAVASCRIPT": -// return createJavaScriptProcedure(ctx, d, meta) -// case "PYTHON": -// return createPythonProcedure(ctx, d, meta) -// case "SCALA": -// return createScalaProcedure(ctx, d, meta) -// case "SQL": -// return createSQLProcedure(ctx, d, meta) -// default: -// return diag.Diagnostics{ -// diag.Diagnostic{ -// Severity: diag.Error, -// Summary: "Invalid language", -// Detail: fmt.Sprintf("Language %s is not supported", lang), -// }, -// } -// } -//} -// -//func createJavaProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// name := d.Get("name").(string) -// schema := d.Get("schema").(string) -// database := d.Get("database").(string) -// id := sdk.NewSchemaObjectIdentifier(database, schema, name) -// -// returns, diags := parseProcedureReturnsRequest(d.Get("return_type").(string)) -// if diags != nil { -// return diags -// } -// procedureDefinition := d.Get("statement").(string) -// runtimeVersion := d.Get("runtime_version").(string) -// packages := []sdk.ProcedurePackageRequest{} -// for _, item := range d.Get("packages").([]interface{}) { -// packages = append(packages, *sdk.NewProcedurePackageRequest(item.(string))) -// } -// handler := d.Get("handler").(string) -// req := sdk.NewCreateForJavaProcedureRequest(id, *returns, runtimeVersion, packages, handler) -// req.WithProcedureDefinition(sdk.String(procedureDefinition)) -// args, diags := getProcedureArguments(d) -// if diags != nil { -// return diags -// } -// if len(args) > 0 { -// req.WithArguments(args) -// } -// -// // read optional params -// if v, ok := d.GetOk("execute_as"); ok { -// if strings.ToUpper(v.(string)) == "OWNER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) -// } else if strings.ToUpper(v.(string)) == "CALLER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) -// } -// } -// if v, ok := d.GetOk("comment"); ok { -// req.WithComment(sdk.String(v.(string))) -// } -// if v, ok := d.GetOk("secure"); ok { -// req.WithSecure(sdk.Bool(v.(bool))) -// } -// if _, ok := d.GetOk("imports"); ok { -// imports := []sdk.ProcedureImportRequest{} -// for _, item := range d.Get("imports").([]interface{}) { -// imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) -// } -// req.WithImports(imports) -// } -// -// if err := client.Procedures.CreateForJava(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, schema, name, argTypes) -// d.SetId(sid.FullyQualifiedName()) -// return ReadContextProcedure(ctx, d, meta) -//} -// -//func createJavaScriptProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// name := d.Get("name").(string) -// schema := d.Get("schema").(string) -// database := d.Get("database").(string) -// id := sdk.NewSchemaObjectIdentifier(database, schema, name) -// -// returnType := d.Get("return_type").(string) -// returnDataType, diags := convertProcedureDataType(returnType) -// if diags != nil { -// return diags -// } -// procedureDefinition := d.Get("statement").(string) -// req := sdk.NewCreateForJavaScriptProcedureRequest(id, returnDataType, procedureDefinition) -// args, diags := getProcedureArguments(d) -// if diags != nil { -// return diags -// } -// if len(args) > 0 { -// req.WithArguments(args) -// } -// -// // read optional params -// if v, ok := d.GetOk("execute_as"); ok { -// if strings.ToUpper(v.(string)) == "OWNER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) -// } else if strings.ToUpper(v.(string)) == "CALLER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) -// } -// } -// if v, ok := d.GetOk("null_input_behavior"); ok { -// req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehavior(v.(string)))) -// } -// if v, ok := d.GetOk("comment"); ok { -// req.WithComment(sdk.String(v.(string))) -// } -// if v, ok := d.GetOk("secure"); ok { -// req.WithSecure(sdk.Bool(v.(bool))) -// } -// -// if err := client.Procedures.CreateForJavaScript(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, schema, name, argTypes) -// d.SetId(sid.FullyQualifiedName()) -// return ReadContextProcedure(ctx, d, meta) -//} -// -//func createScalaProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// name := d.Get("name").(string) -// schema := d.Get("schema").(string) -// database := d.Get("database").(string) -// id := sdk.NewSchemaObjectIdentifier(database, schema, name) -// -// returns, diags := parseProcedureReturnsRequest(d.Get("return_type").(string)) -// if diags != nil { -// return diags -// } -// procedureDefinition := d.Get("statement").(string) -// runtimeVersion := d.Get("runtime_version").(string) -// packages := []sdk.ProcedurePackageRequest{} -// for _, item := range d.Get("packages").([]interface{}) { -// packages = append(packages, *sdk.NewProcedurePackageRequest(item.(string))) -// } -// handler := d.Get("handler").(string) -// req := sdk.NewCreateForScalaProcedureRequest(id, *returns, runtimeVersion, packages, handler) -// req.WithProcedureDefinition(sdk.String(procedureDefinition)) -// args, diags := getProcedureArguments(d) -// if diags != nil { -// return diags -// } -// if len(args) > 0 { -// req.WithArguments(args) -// } -// -// // read optional params -// if v, ok := d.GetOk("execute_as"); ok { -// if strings.ToUpper(v.(string)) == "OWNER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) -// } else if strings.ToUpper(v.(string)) == "CALLER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) -// } -// } -// if v, ok := d.GetOk("comment"); ok { -// req.WithComment(sdk.String(v.(string))) -// } -// if v, ok := d.GetOk("secure"); ok { -// req.WithSecure(sdk.Bool(v.(bool))) -// } -// if _, ok := d.GetOk("imports"); ok { -// imports := []sdk.ProcedureImportRequest{} -// for _, item := range d.Get("imports").([]interface{}) { -// imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) -// } -// req.WithImports(imports) -// } -// -// if err := client.Procedures.CreateForScala(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, schema, name, argTypes) -// d.SetId(sid.FullyQualifiedName()) -// return ReadContextProcedure(ctx, d, meta) -//} -// -//func createSQLProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// name := d.Get("name").(string) -// schema := d.Get("schema").(string) -// database := d.Get("database").(string) -// id := sdk.NewSchemaObjectIdentifier(database, schema, name) -// -// returns, diags := parseProcedureSQLReturnsRequest(d.Get("return_type").(string)) -// if diags != nil { -// return diags -// } -// procedureDefinition := d.Get("statement").(string) -// req := sdk.NewCreateForSQLProcedureRequest(id, *returns, procedureDefinition) -// args, diags := getProcedureArguments(d) -// if diags != nil { -// return diags -// } -// if len(args) > 0 { -// req.WithArguments(args) -// } -// -// // read optional params -// if v, ok := d.GetOk("execute_as"); ok { -// if strings.ToUpper(v.(string)) == "OWNER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) -// } else if strings.ToUpper(v.(string)) == "CALLER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) -// } -// } -// if v, ok := d.GetOk("null_input_behavior"); ok { -// req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehavior(v.(string)))) -// } -// if v, ok := d.GetOk("comment"); ok { -// req.WithComment(sdk.String(v.(string))) -// } -// if v, ok := d.GetOk("secure"); ok { -// req.WithSecure(sdk.Bool(v.(bool))) -// } -// -// if err := client.Procedures.CreateForSQL(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, schema, name, argTypes) -// d.SetId(sid.FullyQualifiedName()) -// return ReadContextProcedure(ctx, d, meta) -//} -// -//func createPythonProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// name := d.Get("name").(string) -// schema := d.Get("schema").(string) -// database := d.Get("database").(string) -// id := sdk.NewSchemaObjectIdentifier(database, schema, name) -// -// returns, diags := parseProcedureReturnsRequest(d.Get("return_type").(string)) -// if diags != nil { -// return diags -// } -// procedureDefinition := d.Get("statement").(string) -// runtimeVersion := d.Get("runtime_version").(string) -// packages := []sdk.ProcedurePackageRequest{} -// for _, item := range d.Get("packages").([]interface{}) { -// packages = append(packages, *sdk.NewProcedurePackageRequest(item.(string))) -// } -// handler := d.Get("handler").(string) -// req := sdk.NewCreateForPythonProcedureRequest(id, *returns, runtimeVersion, packages, handler) -// req.WithProcedureDefinition(sdk.String(procedureDefinition)) -// args, diags := getProcedureArguments(d) -// if diags != nil { -// return diags -// } -// if len(args) > 0 { -// req.WithArguments(args) -// } -// -// // read optional params -// if v, ok := d.GetOk("execute_as"); ok { -// if strings.ToUpper(v.(string)) == "OWNER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) -// } else if strings.ToUpper(v.(string)) == "CALLER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) -// } -// } -// -// // [ { CALLED ON NULL INPUT | { RETURNS NULL ON NULL INPUT | STRICT } } ] does not work for java, scala or python -// // posted in docs-discuss channel, either docs need to be updated to reflect reality or this feature needs to be added -// // https://snowflake.slack.com/archives/C6380540P/p1707511734666249 -// // if v, ok := d.GetOk("null_input_behavior"); ok { -// // req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehavior(v.(string)))) -// // } -// -// if v, ok := d.GetOk("comment"); ok { -// req.WithComment(sdk.String(v.(string))) -// } -// if v, ok := d.GetOk("secure"); ok { -// req.WithSecure(sdk.Bool(v.(bool))) -// } -// if _, ok := d.GetOk("imports"); ok { -// imports := []sdk.ProcedureImportRequest{} -// for _, item := range d.Get("imports").([]interface{}) { -// imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) -// } -// req.WithImports(imports) -// } -// -// if err := client.Procedures.CreateForPython(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, schema, name, argTypes) -// d.SetId(sid.FullyQualifiedName()) -// return ReadContextProcedure(ctx, d, meta) -//} -// -//func ReadContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// diags := diag.Diagnostics{} -// client := meta.(*provider.Context).Client -// -// id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) -// if err := d.Set("name", id.Name()); err != nil { -// return diag.FromErr(err) -// } -// if err := d.Set("database", id.DatabaseName()); err != nil { -// return diag.FromErr(err) -// } -// if err := d.Set("schema", id.SchemaName()); err != nil { -// return diag.FromErr(err) -// } -// args := d.Get("arguments").([]interface{}) -// argTypes := make([]string, len(args)) -// for i, arg := range args { -// argTypes[i] = arg.(map[string]interface{})["type"].(string) -// } -// procedureDetails, err := client.Procedures.Describe(ctx, sdk.NewDescribeProcedureRequest(id.WithoutArguments(), id.Arguments())) -// if err != nil { -// // if procedure is not found then mark resource to be removed from state file during apply or refresh -// d.SetId("") -// return diag.Diagnostics{ -// diag.Diagnostic{ -// Severity: diag.Warning, -// Summary: "Describe procedure failed.", -// Detail: fmt.Sprintf("Describe procedure failed: %v", err), -// }, -// } -// } -// for _, desc := range procedureDetails { -// switch desc.Property { -// case "signature": -// // Format in Snowflake DB is: (argName argType, argName argType, ...) -// args := strings.ReplaceAll(strings.ReplaceAll(desc.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("arguments", args); err != nil { -// return diag.FromErr(err) -// } -// } -// case "null handling": -// if err := d.Set("null_input_behavior", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "body": -// if err := d.Set("statement", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "execute as": -// if err := d.Set("execute_as", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "returns": -// if err := d.Set("return_type", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "language": -// if err := d.Set("language", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "runtime_version": -// if err := d.Set("runtime_version", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "packages": -// packagesString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value, "[", ""), "]", ""), "'", "") -// if packagesString != "" { // Do nothing for Java / Python functions without packages -// packages := strings.Split(packagesString, ",") -// if err := d.Set("packages", packages); err != nil { -// return diag.FromErr(err) -// } -// } -// case "imports": -// importsString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value, "[", ""), "]", ""), "'", ""), " ", "") -// if importsString != "" { // Do nothing for Java functions without imports -// imports := strings.Split(importsString, ",") -// if err := d.Set("imports", imports); err != nil { -// return diag.FromErr(err) -// } -// } -// case "handler": -// if err := d.Set("handler", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// case "volatility": -// if err := d.Set("return_behavior", desc.Value); err != nil { -// return diag.FromErr(err) -// } -// default: -// log.Printf("[INFO] Unexpected procedure property %v returned from Snowflake with value %v", desc.Property, desc.Value) -// } -// } -// -// request := sdk.NewShowProcedureRequest().WithIn(&sdk.In{Schema: sdk.NewDatabaseObjectIdentifier(id.DatabaseName(), id.SchemaName())}).WithLike(&sdk.Like{Pattern: sdk.String(id.Name())}) -// -// procedures, err := client.Procedures.Show(ctx, request) -// if err != nil { -// return diag.FromErr(err) -// } -// // procedure names can be overloaded with different argument types so we iterate over and find the correct one -// // the ShowByID function should probably be updated to also require the list of arg types, like describe procedure -// for _, procedure := range procedures { -// argumentSignature := strings.Split(procedure.Arguments, " RETURN ")[0] -// argumentSignature = strings.ReplaceAll(argumentSignature, " ", "") -// if argumentSignature == id.ArgumentsSignature() { -// if err := d.Set("secure", procedure.IsSecure); err != nil { -// return diag.FromErr(err) -// } -// if err := d.Set("comment", procedure.Description); err != nil { -// return diag.FromErr(err) -// } -// } -// } -// -// return diags -//} -// -//func UpdateContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// -// id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) -// if d.HasChange("name") { -// newId := sdk.NewSchemaObjectIdentifierWithArguments(id.DatabaseName(), id.SchemaName(), d.Get("name").(string), id.Arguments()) -// -// err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithRenameTo(sdk.Pointer(newId.WithoutArguments()))) -// if err != nil { -// return diag.FromErr(err) -// } -// -// d.SetId(newId.FullyQualifiedName()) -// id = newId -// } -// -// if d.HasChange("comment") { -// comment := d.Get("comment") -// if comment != "" { -// if err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithSetComment(sdk.String(comment.(string)))); err != nil { -// return diag.FromErr(err) -// } -// } else { -// if err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithUnsetComment(sdk.Bool(true))); err != nil { -// return diag.FromErr(err) -// } -// } -// } -// -// if d.HasChange("execute_as") { -// req := sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()) -// executeAs := d.Get("execute_as").(string) -// if strings.ToUpper(executeAs) == "OWNER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) -// } else if strings.ToUpper(executeAs) == "CALLER" { -// req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) -// } -// if err := client.Procedures.Alter(ctx, req); err != nil { -// return diag.FromErr(err) -// } -// } -// -// return ReadContextProcedure(ctx, d, meta) -//} -// -//func DeleteContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { -// client := meta.(*provider.Context).Client -// -// id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) -// if err := client.Procedures.Drop(ctx, sdk.NewDropProcedureRequest(id.WithoutArguments(), id.Arguments())); err != nil { -// return diag.FromErr(err) -// } -// d.SetId("") -// return nil -//} -// -//func getProcedureArguments(d *schema.ResourceData) ([]sdk.ProcedureArgumentRequest, diag.Diagnostics) { -// args := make([]sdk.ProcedureArgumentRequest, 0) -// if v, ok := d.GetOk("arguments"); ok { -// for _, arg := range v.([]interface{}) { -// argName := arg.(map[string]interface{})["name"].(string) -// argType := arg.(map[string]interface{})["type"].(string) -// argDataType, diags := convertProcedureDataType(argType) -// if diags != nil { -// return nil, diags -// } -// args = append(args, sdk.ProcedureArgumentRequest{ArgName: argName, ArgDataType: argDataType}) -// } -// } -// return args, nil -//} -// -//func convertProcedureDataType(s string) (sdk.DataType, diag.Diagnostics) { -// dataType, err := sdk.ToDataType(s) -// if err != nil { -// return dataType, diag.FromErr(err) -// } -// return dataType, nil -//} -// -//func convertProcedureColumns(s string) ([]sdk.ProcedureColumn, diag.Diagnostics) { -// pattern := regexp.MustCompile(`(\w+)\s+(\w+)`) -// matches := pattern.FindAllStringSubmatch(s, -1) -// var columns []sdk.ProcedureColumn -// for _, match := range matches { -// if len(match) == 3 { -// dataType, err := sdk.ToDataType(match[2]) -// if err != nil { -// return nil, diag.FromErr(err) -// } -// columns = append(columns, sdk.ProcedureColumn{ -// ColumnName: match[1], -// ColumnDataType: dataType, -// }) -// } -// } -// return columns, nil -//} -// -//func parseProcedureReturnsRequest(s string) (*sdk.ProcedureReturnsRequest, diag.Diagnostics) { -// returns := sdk.NewProcedureReturnsRequest() -// if strings.HasPrefix(strings.ToLower(s), "table") { -// columns, diags := convertProcedureColumns(s) -// if diags != nil { -// return nil, diags -// } -// var cr []sdk.ProcedureColumnRequest -// for _, item := range columns { -// cr = append(cr, *sdk.NewProcedureColumnRequest(item.ColumnName, item.ColumnDataType)) -// } -// returns.WithTable(sdk.NewProcedureReturnsTableRequest().WithColumns(cr)) -// } else { -// returnDataType, diags := convertProcedureDataType(s) -// if diags != nil { -// return nil, diags -// } -// returns.WithResultDataType(sdk.NewProcedureReturnsResultDataTypeRequest(returnDataType)) -// } -// return returns, nil -//} -// -//func parseProcedureSQLReturnsRequest(s string) (*sdk.ProcedureSQLReturnsRequest, diag.Diagnostics) { -// returns := sdk.NewProcedureSQLReturnsRequest() -// if strings.HasPrefix(strings.ToLower(s), "table") { -// columns, diags := convertProcedureColumns(s) -// if diags != nil { -// return nil, diags -// } -// var cr []sdk.ProcedureColumnRequest -// for _, item := range columns { -// cr = append(cr, *sdk.NewProcedureColumnRequest(item.ColumnName, item.ColumnDataType)) -// } -// returns.WithTable(sdk.NewProcedureReturnsTableRequest().WithColumns(cr)) -// } else { -// returnDataType, diags := convertProcedureDataType(s) -// if diags != nil { -// return nil, diags -// } -// returns.WithResultDataType(sdk.NewProcedureReturnsResultDataTypeRequest(returnDataType)) -// } -// return returns, nil -//} +import ( + "context" + "fmt" + "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" + "log" + "regexp" + "slices" + "strings" +) + +var procedureSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Specifies the identifier for the procedure; does not have to be unique for the schema in which the procedure is created. Don't use the | character.", + }, + "database": { + Type: schema.TypeString, + Required: true, + Description: "The database in which to create the procedure. Don't use the | character.", + ForceNew: true, + }, + "schema": { + Type: schema.TypeString, + Required: true, + Description: "The schema in which to create the procedure. Don't use the | character.", + ForceNew: true, + }, + "secure": { + Type: schema.TypeBool, + Optional: true, + Description: "Specifies that the procedure is secure. For more information about secure procedures, see Protecting Sensitive Information with Secure UDFs and Stored Procedures.", + Default: false, + }, + "arguments": { + Type: schema.TypeList, + 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(old, new) + }, + Description: "The argument name", + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: dataTypeValidateFunc, + DiffSuppressFunc: dataTypeDiffSuppressFunc, + Description: "The argument type", + }, + }, + }, + Optional: true, + Description: "List of the arguments for the procedure", + ForceNew: true, + }, + "return_type": { + Type: schema.TypeString, + Description: "The return type of the procedure", + // 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 { + if strings.EqualFold(old, new) { + return true + } + + varcharType := []string{"VARCHAR(16777216)", "VARCHAR", "text", "string", "NVARCHAR", "NVARCHAR2", "CHAR VARYING", "NCHAR VARYING"} + if slices.Contains(varcharType, strings.ToUpper(old)) && slices.Contains(varcharType, strings.ToUpper(new)) { + return true + } + + // all these types are equivalent https://docs.snowflake.com/en/sql-reference/data-types-numeric.html#int-integer-bigint-smallint-tinyint-byteint + integerTypes := []string{"INT", "INTEGER", "BIGINT", "SMALLINT", "TINYINT", "BYTEINT", "NUMBER(38,0)"} + if slices.Contains(integerTypes, strings.ToUpper(old)) && slices.Contains(integerTypes, strings.ToUpper(new)) { + return true + } + return false + }, + Required: true, + ForceNew: true, + }, + "statement": { + Type: schema.TypeString, + Required: true, + Description: "Specifies the code used to create the procedure.", + ForceNew: true, + DiffSuppressFunc: DiffSuppressStatement, + }, + "language": { + Type: schema.TypeString, + Optional: true, + Default: "SQL", + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + ValidateFunc: validation.StringInSlice([]string{"javascript", "java", "scala", "SQL", "python"}, true), + Description: "Specifies the language of the stored procedure code.", + }, + "execute_as": { + Type: schema.TypeString, + Optional: true, + Default: "OWNER", + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return strings.EqualFold(old, new) + }, + ValidateFunc: validation.StringInSlice([]string{"CALLER", "OWNER"}, true), + Description: "Sets execution context. Allowed values are CALLER and OWNER (consult a proper section in the [docs](https://docs.snowflake.com/en/sql-reference/sql/create-procedure#id1)). For more information see [caller's rights and owner's rights](https://docs.snowflake.com/en/developer-guide/stored-procedure/stored-procedures-rights).", + }, + "null_input_behavior": { + Type: schema.TypeString, + Optional: true, + Default: "CALLED ON NULL INPUT", + ForceNew: true, + // We do not use STRICT, because Snowflake then in the Read phase returns RETURNS NULL ON NULL INPUT + ValidateFunc: validation.StringInSlice([]string{"CALLED ON NULL INPUT", "RETURNS NULL ON NULL INPUT"}, false), + Description: "Specifies the behavior of the procedure when called with null inputs.", + }, + "return_behavior": { + Type: schema.TypeString, + Optional: true, + Default: "VOLATILE", + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"VOLATILE", "IMMUTABLE"}, false), + Description: "Specifies the behavior of the function when returning results", + Deprecated: "These keywords are deprecated for stored procedures. These keywords are not intended to apply to stored procedures. In a future release, these keywords will be removed from the documentation.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Default: "user-defined procedure", + Description: "Specifies a comment for the procedure.", + }, + "runtime_version": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Required for Python procedures. Specifies Python runtime version.", + }, + "packages": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: true, + Description: "List of package imports to use for Java / Python procedures. For Java, package imports should be of the form: package_name:version_number, where package_name is snowflake_domain:package. For Python use it should be: ('numpy','pandas','xgboost==1.5.0').", + }, + "imports": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: true, + Description: "Imports for Java / Python procedures. For Java this a list of jar files, for Python this is a list of Python files.", + }, + "handler": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The handler method for Java / Python procedures.", + }, +} + +// Procedure returns a pointer to the resource representing a stored procedure. +func Procedure() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + CreateContext: CreateContextProcedure, + ReadContext: ReadContextProcedure, + UpdateContext: UpdateContextProcedure, + DeleteContext: DeleteContextProcedure, + + Schema: procedureSchema, + 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: v085ProcedureStateUpgrader, + }, + }, + } +} + +func CreateContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + lang := strings.ToUpper(d.Get("language").(string)) + switch lang { + case "JAVA": + return createJavaProcedure(ctx, d, meta) + case "JAVASCRIPT": + return createJavaScriptProcedure(ctx, d, meta) + case "PYTHON": + return createPythonProcedure(ctx, d, meta) + case "SCALA": + return createScalaProcedure(ctx, d, meta) + case "SQL": + return createSQLProcedure(ctx, d, meta) + default: + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid language", + Detail: fmt.Sprintf("Language %s is not supported", lang), + }, + } + } +} + +func createJavaProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) + + returns, diags := parseProcedureReturnsRequest(d.Get("return_type").(string)) + if diags != nil { + return diags + } + procedureDefinition := d.Get("statement").(string) + runtimeVersion := d.Get("runtime_version").(string) + packages := []sdk.ProcedurePackageRequest{} + for _, item := range d.Get("packages").([]interface{}) { + packages = append(packages, *sdk.NewProcedurePackageRequest(item.(string))) + } + handler := d.Get("handler").(string) + req := sdk.NewCreateForJavaProcedureRequest(id, *returns, runtimeVersion, packages, handler) + req.WithProcedureDefinition(sdk.String(procedureDefinition)) + args, diags := getProcedureArguments(d) + if diags != nil { + return diags + } + if len(args) > 0 { + req.WithArguments(args) + } + + // read optional params + if v, ok := d.GetOk("execute_as"); ok { + if strings.ToUpper(v.(string)) == "OWNER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) + } else if strings.ToUpper(v.(string)) == "CALLER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) + } + } + if v, ok := d.GetOk("comment"); ok { + req.WithComment(sdk.String(v.(string))) + } + if v, ok := d.GetOk("secure"); ok { + req.WithSecure(sdk.Bool(v.(bool))) + } + if _, ok := d.GetOk("imports"); ok { + imports := []sdk.ProcedureImportRequest{} + for _, item := range d.Get("imports").([]interface{}) { + imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) + } + req.WithImports(imports) + } + + if err := client.Procedures.CreateForJava(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.NewSchemaObjectIdentifierWithArgumentsOld(database, schema, name, argTypes) + d.SetId(sid.FullyQualifiedName()) + return ReadContextProcedure(ctx, d, meta) +} + +func createJavaScriptProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) + + returnType := d.Get("return_type").(string) + returnDataType, diags := convertProcedureDataType(returnType) + if diags != nil { + return diags + } + procedureDefinition := d.Get("statement").(string) + req := sdk.NewCreateForJavaScriptProcedureRequest(id, returnDataType, procedureDefinition) + args, diags := getProcedureArguments(d) + if diags != nil { + return diags + } + if len(args) > 0 { + req.WithArguments(args) + } + + // read optional params + if v, ok := d.GetOk("execute_as"); ok { + if strings.ToUpper(v.(string)) == "OWNER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) + } else if strings.ToUpper(v.(string)) == "CALLER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) + } + } + if v, ok := d.GetOk("null_input_behavior"); ok { + req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehavior(v.(string)))) + } + if v, ok := d.GetOk("comment"); ok { + req.WithComment(sdk.String(v.(string))) + } + if v, ok := d.GetOk("secure"); ok { + req.WithSecure(sdk.Bool(v.(bool))) + } + + if err := client.Procedures.CreateForJavaScript(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.NewSchemaObjectIdentifierWithArgumentsOld(database, schema, name, argTypes) + d.SetId(sid.FullyQualifiedName()) + return ReadContextProcedure(ctx, d, meta) +} + +func createScalaProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) + + returns, diags := parseProcedureReturnsRequest(d.Get("return_type").(string)) + if diags != nil { + return diags + } + procedureDefinition := d.Get("statement").(string) + runtimeVersion := d.Get("runtime_version").(string) + packages := []sdk.ProcedurePackageRequest{} + for _, item := range d.Get("packages").([]interface{}) { + packages = append(packages, *sdk.NewProcedurePackageRequest(item.(string))) + } + handler := d.Get("handler").(string) + req := sdk.NewCreateForScalaProcedureRequest(id, *returns, runtimeVersion, packages, handler) + req.WithProcedureDefinition(sdk.String(procedureDefinition)) + args, diags := getProcedureArguments(d) + if diags != nil { + return diags + } + if len(args) > 0 { + req.WithArguments(args) + } + + // read optional params + if v, ok := d.GetOk("execute_as"); ok { + if strings.ToUpper(v.(string)) == "OWNER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) + } else if strings.ToUpper(v.(string)) == "CALLER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) + } + } + if v, ok := d.GetOk("comment"); ok { + req.WithComment(sdk.String(v.(string))) + } + if v, ok := d.GetOk("secure"); ok { + req.WithSecure(sdk.Bool(v.(bool))) + } + if _, ok := d.GetOk("imports"); ok { + imports := []sdk.ProcedureImportRequest{} + for _, item := range d.Get("imports").([]interface{}) { + imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) + } + req.WithImports(imports) + } + + if err := client.Procedures.CreateForScala(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.NewSchemaObjectIdentifierWithArgumentsOld(database, schema, name, argTypes) + d.SetId(sid.FullyQualifiedName()) + return ReadContextProcedure(ctx, d, meta) +} + +func createSQLProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) + + returns, diags := parseProcedureSQLReturnsRequest(d.Get("return_type").(string)) + if diags != nil { + return diags + } + procedureDefinition := d.Get("statement").(string) + req := sdk.NewCreateForSQLProcedureRequest(id, *returns, procedureDefinition) + args, diags := getProcedureArguments(d) + if diags != nil { + return diags + } + if len(args) > 0 { + req.WithArguments(args) + } + + // read optional params + if v, ok := d.GetOk("execute_as"); ok { + if strings.ToUpper(v.(string)) == "OWNER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) + } else if strings.ToUpper(v.(string)) == "CALLER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) + } + } + if v, ok := d.GetOk("null_input_behavior"); ok { + req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehavior(v.(string)))) + } + if v, ok := d.GetOk("comment"); ok { + req.WithComment(sdk.String(v.(string))) + } + if v, ok := d.GetOk("secure"); ok { + req.WithSecure(sdk.Bool(v.(bool))) + } + + if err := client.Procedures.CreateForSQL(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.NewSchemaObjectIdentifierWithArgumentsOld(database, schema, name, argTypes) + d.SetId(sid.FullyQualifiedName()) + return ReadContextProcedure(ctx, d, meta) +} + +func createPythonProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) + + returns, diags := parseProcedureReturnsRequest(d.Get("return_type").(string)) + if diags != nil { + return diags + } + procedureDefinition := d.Get("statement").(string) + runtimeVersion := d.Get("runtime_version").(string) + packages := []sdk.ProcedurePackageRequest{} + for _, item := range d.Get("packages").([]interface{}) { + packages = append(packages, *sdk.NewProcedurePackageRequest(item.(string))) + } + handler := d.Get("handler").(string) + req := sdk.NewCreateForPythonProcedureRequest(id, *returns, runtimeVersion, packages, handler) + req.WithProcedureDefinition(sdk.String(procedureDefinition)) + args, diags := getProcedureArguments(d) + if diags != nil { + return diags + } + if len(args) > 0 { + req.WithArguments(args) + } + + // read optional params + if v, ok := d.GetOk("execute_as"); ok { + if strings.ToUpper(v.(string)) == "OWNER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) + } else if strings.ToUpper(v.(string)) == "CALLER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) + } + } + + // [ { CALLED ON NULL INPUT | { RETURNS NULL ON NULL INPUT | STRICT } } ] does not work for java, scala or python + // posted in docs-discuss channel, either docs need to be updated to reflect reality or this feature needs to be added + // https://snowflake.slack.com/archives/C6380540P/p1707511734666249 + // if v, ok := d.GetOk("null_input_behavior"); ok { + // req.WithNullInputBehavior(sdk.Pointer(sdk.NullInputBehavior(v.(string)))) + // } + + if v, ok := d.GetOk("comment"); ok { + req.WithComment(sdk.String(v.(string))) + } + if v, ok := d.GetOk("secure"); ok { + req.WithSecure(sdk.Bool(v.(bool))) + } + if _, ok := d.GetOk("imports"); ok { + imports := []sdk.ProcedureImportRequest{} + for _, item := range d.Get("imports").([]interface{}) { + imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) + } + req.WithImports(imports) + } + + if err := client.Procedures.CreateForPython(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.NewSchemaObjectIdentifierWithArgumentsOld(database, schema, name, argTypes) + d.SetId(sid.FullyQualifiedName()) + return ReadContextProcedure(ctx, d, meta) +} + +func ReadContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} + client := meta.(*provider.Context).Client + + id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) + if err := d.Set("name", id.Name()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("database", id.DatabaseName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("schema", id.SchemaName()); err != nil { + return diag.FromErr(err) + } + args := d.Get("arguments").([]interface{}) + argTypes := make([]string, len(args)) + for i, arg := range args { + argTypes[i] = arg.(map[string]interface{})["type"].(string) + } + procedureDetails, err := client.Procedures.Describe(ctx, sdk.NewDescribeProcedureRequest(id.WithoutArguments(), id.Arguments())) + if err != nil { + // if procedure is not found then mark resource to be removed from state file during apply or refresh + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Describe procedure failed.", + Detail: fmt.Sprintf("Describe procedure failed: %v", err), + }, + } + } + for _, desc := range procedureDetails { + switch desc.Property { + case "signature": + // Format in Snowflake DB is: (argName argType, argName argType, ...) + args := strings.ReplaceAll(strings.ReplaceAll(desc.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("arguments", args); err != nil { + return diag.FromErr(err) + } + } + case "null handling": + if err := d.Set("null_input_behavior", desc.Value); err != nil { + return diag.FromErr(err) + } + case "body": + if err := d.Set("statement", desc.Value); err != nil { + return diag.FromErr(err) + } + case "execute as": + if err := d.Set("execute_as", desc.Value); err != nil { + return diag.FromErr(err) + } + case "returns": + if err := d.Set("return_type", desc.Value); err != nil { + return diag.FromErr(err) + } + case "language": + if err := d.Set("language", desc.Value); err != nil { + return diag.FromErr(err) + } + case "runtime_version": + if err := d.Set("runtime_version", desc.Value); err != nil { + return diag.FromErr(err) + } + case "packages": + packagesString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value, "[", ""), "]", ""), "'", "") + if packagesString != "" { // Do nothing for Java / Python functions without packages + packages := strings.Split(packagesString, ",") + if err := d.Set("packages", packages); err != nil { + return diag.FromErr(err) + } + } + case "imports": + importsString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value, "[", ""), "]", ""), "'", ""), " ", "") + if importsString != "" { // Do nothing for Java functions without imports + imports := strings.Split(importsString, ",") + if err := d.Set("imports", imports); err != nil { + return diag.FromErr(err) + } + } + case "handler": + if err := d.Set("handler", desc.Value); err != nil { + return diag.FromErr(err) + } + case "volatility": + if err := d.Set("return_behavior", desc.Value); err != nil { + return diag.FromErr(err) + } + default: + log.Printf("[INFO] Unexpected procedure property %v returned from Snowflake with value %v", desc.Property, desc.Value) + } + } + + request := sdk.NewShowProcedureRequest().WithIn(&sdk.In{Schema: sdk.NewDatabaseObjectIdentifier(id.DatabaseName(), id.SchemaName())}).WithLike(&sdk.Like{Pattern: sdk.String(id.Name())}) + + procedures, err := client.Procedures.Show(ctx, request) + if err != nil { + return diag.FromErr(err) + } + // procedure names can be overloaded with different argument types so we iterate over and find the correct one + // the ShowByID function should probably be updated to also require the list of arg types, like describe procedure + for _, procedure := range procedures { + argumentSignature := strings.Split(procedure.Arguments, " RETURN ")[0] + argumentSignature = strings.ReplaceAll(argumentSignature, " ", "") + if argumentSignature == id.ArgumentsSignature() { + if err := d.Set("secure", procedure.IsSecure); err != nil { + return diag.FromErr(err) + } + if err := d.Set("comment", procedure.Description); err != nil { + return diag.FromErr(err) + } + } + } + + return diags +} + +func UpdateContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + + id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) + if d.HasChange("name") { + newId := sdk.NewSchemaObjectIdentifierWithArgumentsOld(id.DatabaseName(), id.SchemaName(), d.Get("name").(string), id.Arguments()) + + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithRenameTo(sdk.Pointer(newId.WithoutArguments()))) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(newId.FullyQualifiedName()) + id = newId + } + + if d.HasChange("comment") { + comment := d.Get("comment") + if comment != "" { + if err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithSetComment(sdk.String(comment.(string)))); err != nil { + return diag.FromErr(err) + } + } else { + if err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithUnsetComment(sdk.Bool(true))); err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("execute_as") { + req := sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()) + executeAs := d.Get("execute_as").(string) + if strings.ToUpper(executeAs) == "OWNER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsOwner)) + } else if strings.ToUpper(executeAs) == "CALLER" { + req.WithExecuteAs(sdk.Pointer(sdk.ExecuteAsCaller)) + } + if err := client.Procedures.Alter(ctx, req); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextProcedure(ctx, d, meta) +} + +func DeleteContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*provider.Context).Client + + id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) + if err := client.Procedures.Drop(ctx, sdk.NewDropProcedureRequest(id.WithoutArguments(), id.Arguments())); err != nil { + return diag.FromErr(err) + } + d.SetId("") + return nil +} + +func getProcedureArguments(d *schema.ResourceData) ([]sdk.ProcedureArgumentRequest, diag.Diagnostics) { + args := make([]sdk.ProcedureArgumentRequest, 0) + if v, ok := d.GetOk("arguments"); ok { + for _, arg := range v.([]interface{}) { + argName := arg.(map[string]interface{})["name"].(string) + argType := arg.(map[string]interface{})["type"].(string) + argDataType, diags := convertProcedureDataType(argType) + if diags != nil { + return nil, diags + } + args = append(args, sdk.ProcedureArgumentRequest{ArgName: argName, ArgDataType: argDataType}) + } + } + return args, nil +} + +func convertProcedureDataType(s string) (sdk.DataType, diag.Diagnostics) { + dataType, err := sdk.ToDataType(s) + if err != nil { + return dataType, diag.FromErr(err) + } + return dataType, nil +} + +func convertProcedureColumns(s string) ([]sdk.ProcedureColumn, diag.Diagnostics) { + pattern := regexp.MustCompile(`(\w+)\s+(\w+)`) + matches := pattern.FindAllStringSubmatch(s, -1) + var columns []sdk.ProcedureColumn + for _, match := range matches { + if len(match) == 3 { + dataType, err := sdk.ToDataType(match[2]) + if err != nil { + return nil, diag.FromErr(err) + } + columns = append(columns, sdk.ProcedureColumn{ + ColumnName: match[1], + ColumnDataType: dataType, + }) + } + } + return columns, nil +} + +func parseProcedureReturnsRequest(s string) (*sdk.ProcedureReturnsRequest, diag.Diagnostics) { + returns := sdk.NewProcedureReturnsRequest() + if strings.HasPrefix(strings.ToLower(s), "table") { + columns, diags := convertProcedureColumns(s) + if diags != nil { + return nil, diags + } + var cr []sdk.ProcedureColumnRequest + for _, item := range columns { + cr = append(cr, *sdk.NewProcedureColumnRequest(item.ColumnName, item.ColumnDataType)) + } + returns.WithTable(sdk.NewProcedureReturnsTableRequest().WithColumns(cr)) + } else { + returnDataType, diags := convertProcedureDataType(s) + if diags != nil { + return nil, diags + } + returns.WithResultDataType(sdk.NewProcedureReturnsResultDataTypeRequest(returnDataType)) + } + return returns, nil +} + +func parseProcedureSQLReturnsRequest(s string) (*sdk.ProcedureSQLReturnsRequest, diag.Diagnostics) { + returns := sdk.NewProcedureSQLReturnsRequest() + if strings.HasPrefix(strings.ToLower(s), "table") { + columns, diags := convertProcedureColumns(s) + if diags != nil { + return nil, diags + } + var cr []sdk.ProcedureColumnRequest + for _, item := range columns { + cr = append(cr, *sdk.NewProcedureColumnRequest(item.ColumnName, item.ColumnDataType)) + } + returns.WithTable(sdk.NewProcedureReturnsTableRequest().WithColumns(cr)) + } else { + returnDataType, diags := convertProcedureDataType(s) + if diags != nil { + return nil, diags + } + returns.WithResultDataType(sdk.NewProcedureReturnsResultDataTypeRequest(returnDataType)) + } + return returns, nil +} diff --git a/pkg/resources/procedure_acceptance_test.go b/pkg/resources/procedure_acceptance_test.go index 1f13f783a2e..fe5ca5283ef 100644 --- a/pkg/resources/procedure_acceptance_test.go +++ b/pkg/resources/procedure_acceptance_test.go @@ -1,376 +1,389 @@ package resources_test -//func testAccProcedure(t *testing.T, configDirectory string) { -// t.Helper() -// -// name := acc.TestClient().Ids.Alpha() -// newName := acc.TestClient().Ids.Alpha() -// -// resourceName := "snowflake_procedure.p" -// m := func() map[string]config.Variable { -// return map[string]config.Variable{ -// "name": config.StringVariable(name), -// "database": config.StringVariable(acc.TestDatabaseName), -// "schema": config.StringVariable(acc.TestSchemaName), -// "comment": config.StringVariable("Terraform acceptance test"), -// "execute_as": config.StringVariable("CALLER"), -// } -// } -// variableSet2 := m() -// variableSet2["name"] = config.StringVariable(newName) -// variableSet2["comment"] = config.StringVariable("Terraform acceptance test - updated") -// variableSet2["execute_as"] = config.StringVariable("OWNER") -// -// ignoreDuringImport := []string{"null_input_behavior"} -// if strings.Contains(configDirectory, "/sql") { -// ignoreDuringImport = append(ignoreDuringImport, "return_behavior") -// } -// -// 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.Procedure), -// Steps: []resource.TestStep{ -// { -// ConfigDirectory: acc.ConfigurationDirectory(configDirectory), -// ConfigVariables: m(), -// Check: resource.ComposeTestCheckFunc( -// resource.TestCheckResourceAttr(resourceName, "name", name), -// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), -// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), -// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), -// resource.TestCheckResourceAttr(resourceName, "return_behavior", "VOLATILE"), -// resource.TestCheckResourceAttr(resourceName, "execute_as", "CALLER"), -// -// // computed attributes -// resource.TestCheckResourceAttrSet(resourceName, "return_type"), -// resource.TestCheckResourceAttrSet(resourceName, "statement"), -// resource.TestCheckResourceAttrSet(resourceName, "secure"), -// ), -// }, -// -// // test - rename + change comment and caller (proves https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2642) -// { -// ConfigDirectory: acc.ConfigurationDirectory(configDirectory), -// ConfigVariables: variableSet2, -// ConfigPlanChecks: resource.ConfigPlanChecks{ -// PreApply: []plancheck.PlanCheck{ -// plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), -// }, -// }, -// Check: resource.ComposeTestCheckFunc( -// resource.TestCheckResourceAttr(resourceName, "name", newName), -// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), -// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), -// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), -// resource.TestCheckResourceAttr(resourceName, "execute_as", "OWNER"), -// ), -// }, -// -// // test - import -// { -// ConfigDirectory: acc.ConfigurationDirectory(configDirectory), -// ConfigVariables: variableSet2, -// ResourceName: resourceName, -// ImportState: true, -// ImportStateVerify: true, -// ImportStateVerifyIgnore: ignoreDuringImport, -// }, -// }, -// }) -//} -// -//func TestAcc_Procedure_SQL(t *testing.T) { -// testAccProcedure(t, "TestAcc_Procedure/sql") -//} -// -///* -//Error: 391531 (42601): SQL compilation error: An active warehouse is required for creating Python stored procedures. -//func TestAcc_Procedure_Python(t *testing.T) { -// testAccProcedure(t, "TestAcc_Procedure/python") -//} -//*/ -// -//func TestAcc_Procedure_Javascript(t *testing.T) { -// testAccProcedure(t, "TestAcc_Procedure/javascript") -//} -// -//func TestAcc_Procedure_Java(t *testing.T) { -// testAccProcedure(t, "TestAcc_Procedure/java") -//} -// -//func TestAcc_Procedure_Scala(t *testing.T) { -// testAccProcedure(t, "TestAcc_Procedure/scala") -//} -// -//func TestAcc_Procedure_complex(t *testing.T) { -// name := acc.TestClient().Ids.Alpha() -// resourceName := "snowflake_procedure.p" -// m := func() map[string]config.Variable { -// return map[string]config.Variable{ -// "name": config.StringVariable(name), -// "database": config.StringVariable(acc.TestDatabaseName), -// "schema": config.StringVariable(acc.TestSchemaName), -// "comment": config.StringVariable("Terraform acceptance test"), -// "execute_as": config.StringVariable("CALLER"), -// } -// } -// variableSet2 := m() -// variableSet2["comment"] = config.StringVariable("Terraform acceptance test - updated") -// -// statement := "var x = 1\nreturn x\n" -// 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.Procedure), -// Steps: []resource.TestStep{ -// { -// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Procedure/complex"), -// ConfigVariables: m(), -// Check: resource.ComposeTestCheckFunc( -// resource.TestCheckResourceAttr(resourceName, "name", name), -// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), -// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), -// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), -// resource.TestCheckResourceAttr(resourceName, "statement", statement), -// resource.TestCheckResourceAttr(resourceName, "execute_as", "CALLER"), -// resource.TestCheckResourceAttr(resourceName, "arguments.#", "2"), -// resource.TestCheckResourceAttr(resourceName, "arguments.0.name", "ARG1"), -// resource.TestCheckResourceAttr(resourceName, "arguments.0.type", "VARCHAR"), -// resource.TestCheckResourceAttr(resourceName, "arguments.1.name", "ARG2"), -// resource.TestCheckResourceAttr(resourceName, "arguments.1.type", "DATE"), -// resource.TestCheckResourceAttr(resourceName, "null_input_behavior", "RETURNS NULL ON NULL INPUT"), -// -// // computed attributes -// resource.TestCheckResourceAttrSet(resourceName, "return_type"), -// resource.TestCheckResourceAttrSet(resourceName, "statement"), -// resource.TestCheckResourceAttrSet(resourceName, "execute_as"), -// resource.TestCheckResourceAttrSet(resourceName, "secure"), -// ), -// }, -// -// // test - change comment -// { -// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Procedure/complex"), -// ConfigVariables: variableSet2, -// Check: resource.ComposeTestCheckFunc( -// resource.TestCheckResourceAttr(resourceName, "name", name), -// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), -// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), -// resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test - updated"), -// ), -// }, -// -// // test - import -// { -// ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Procedure/complex"), -// ConfigVariables: variableSet2, -// ResourceName: resourceName, -// ImportState: true, -// ImportStateVerify: true, -// ImportStateVerifyIgnore: []string{ -// "return_behavior", -// }, -// }, -// }, -// }) -//} -// -//func TestAcc_Procedure_migrateFromVersion085(t *testing.T) { -// id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() -// name := id.Name() -// resourceName := "snowflake_procedure.p" -// -// resource.Test(t, resource.TestCase{ -// PreCheck: func() { acc.TestAccPreCheck(t) }, -// TerraformVersionChecks: []tfversion.TerraformVersionCheck{ -// tfversion.RequireAbove(tfversion.Version1_5_0), -// }, -// CheckDestroy: acc.CheckDestroy(t, resources.Procedure), -// -// Steps: []resource.TestStep{ -// { -// ExternalProviders: map[string]resource.ExternalProvider{ -// "snowflake": { -// VersionConstraint: "=0.85.0", -// Source: "Snowflake-Labs/snowflake", -// }, -// }, -// Config: procedureConfig(acc.TestDatabaseName, acc.TestSchemaName, name), -// Check: resource.ComposeTestCheckFunc( -// resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%s|%s|%s|", acc.TestDatabaseName, acc.TestSchemaName, name)), -// resource.TestCheckResourceAttr(resourceName, "name", name), -// resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), -// resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), -// ), -// }, -// { -// ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, -// Config: procedureConfig(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), -// ), -// }, -// }, -// }) -//} -// -//func procedureConfig(database string, schema string, name string) string { -// return fmt.Sprintf(` -//resource "snowflake_procedure" "p" { -// database = "%[1]s" -// schema = "%[2]s" -// name = "%[3]s" -// language = "JAVASCRIPT" -// return_type = "VARCHAR" -// statement = <