diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index c204f3ec7d..bc5ee14e30 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -1,6 +1,6 @@ # Migration guide -This document is meant to help you migrate your Terraform config to the new newest version. In migration guides, we will only +This document is meant to help you migrate your Terraform config to the new newest version. In migration guides, we will only describe deprecations or breaking changes and help you to change your configuration to keep the same (or similar) behavior across different versions. @@ -22,6 +22,14 @@ In recent changes, we introduced a new grant resources to replace the old ones. To aid with the migration, we wrote a guide to show one of the possible ways to migrate deprecated resources to their new counter-parts. As the guide is more general and applies to every version (and provider), we moved it [here](./docs/technical-documentation/resource_migration.md). +### snowflake_procedure resource changes +#### *(deprecation)* return_behavior +`return_behavior` parameter is deprecated because it is also deprecated in the Snowflake API. + +### snowflake_function resource changes +#### *(behavior change)* return_type +`return_type` has become force new because there is no way to alter it without dropping and recreating the function. + ## v0.84.0 ➞ v0.85.0 ### snowflake_notification_integration resource changes @@ -47,7 +55,7 @@ Force new was added for the following attributes (because no usable SQL alter st ## v0.73.0 ➞ v0.74.0 ### Provider configuration changes -In this change we have done a provider refactor to make it more complete and customizable by supporting more options that +In this change we have done a provider refactor to make it more complete and customizable by supporting more options that were already available in Golang Snowflake driver. This lead to several attributes being added and a few deprecated. We will focus on the deprecated ones and show you how to adapt your current configuration to the new changes. @@ -57,7 +65,7 @@ We will focus on the deprecated ones and show you how to adapt your current conf provider "snowflake" { # before username = "username" - + # after user = "username" } @@ -123,7 +131,7 @@ provider "snowflake" { provider "snowflake" { # before session_params = {} - + # after params = {} } diff --git a/docs/resources/procedure.md b/docs/resources/procedure.md index 301a67c24c..2e45291642 100644 --- a/docs/resources/procedure.md +++ b/docs/resources/procedure.md @@ -70,8 +70,9 @@ EOT - `language` (String) Specifies the language of the stored procedure code. - `null_input_behavior` (String) Specifies the behavior of the procedure when called with null inputs. - `packages` (List of String) 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'). -- `return_behavior` (String) Specifies the behavior of the function when returning results +- `return_behavior` (String, Deprecated) Specifies the behavior of the function when returning results - `runtime_version` (String) Required for Python procedures. Specifies Python runtime version. +- `secure` (Boolean) Specifies that the procedure is secure. For more information about secure procedures, see Protecting Sensitive Information with Secure UDFs and Stored Procedures. ### Read-Only diff --git a/pkg/datasources/procedures.go b/pkg/datasources/procedures.go index 18939f725d..d72068bbef 100644 --- a/pkg/datasources/procedures.go +++ b/pkg/datasources/procedures.go @@ -1,14 +1,14 @@ package datasources import ( + "context" "database/sql" - "errors" "fmt" - "log" "regexp" "strings" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -64,50 +64,59 @@ var proceduresSchema = map[string]*schema.Schema{ func Procedures() *schema.Resource { return &schema.Resource{ - Read: ReadProcedures, - Schema: proceduresSchema, + ReadContext: ReadContextProcedures, + Schema: proceduresSchema, } } -func ReadProcedures(d *schema.ResourceData, meta interface{}) error { +func ReadContextProcedures(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) databaseName := d.Get("database").(string) schemaName := d.Get("schema").(string) - currentProcedures, err := snowflake.ListProcedures(databaseName, schemaName, db) - if errors.Is(err, sql.ErrNoRows) { - // If not found, mark resource to be removed from state file during apply or refresh - log.Printf("[DEBUG] procedures in schema (%s) not found", d.Id()) - d.SetId("") - return nil - } else if err != nil { - log.Printf("[DEBUG] unable to parse procedures in schema (%s)", d.Id()) - d.SetId("") - return nil + req := sdk.NewShowProcedureRequest() + if databaseName != "" { + req.WithIn(&sdk.In{Database: sdk.NewAccountObjectIdentifier(databaseName)}) + } + if schemaName != "" { + req.WithIn(&sdk.In{Schema: sdk.NewDatabaseObjectIdentifier(databaseName, schemaName)}) } + procedures, err := client.Procedures.Show(ctx, req) + if err != nil { + id := fmt.Sprintf(`%v|%v`, databaseName, schemaName) - procedures := []map[string]interface{}{} + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("Unable to parse procedures in schema (%s)", id), + Detail: "See our document on design decisions for procedures: ", + }, + } + } + proceduresList := []map[string]interface{}{} - for _, procedure := range currentProcedures { + for _, procedure := range procedures { procedureMap := map[string]interface{}{} - - procedureSignatureMap, err := parseArguments(procedure.Arguments.String) + procedureMap["name"] = procedure.Name + procedureMap["database"] = procedure.CatalogName + procedureMap["schema"] = procedure.SchemaName + procedureMap["comment"] = procedure.Description + procedureSignatureMap, err := parseArguments(procedure.Arguments) if err != nil { - return err + return diag.FromErr(err) } - - procedureMap["name"] = procedure.Name.String - procedureMap["database"] = procedure.DatabaseName.String - procedureMap["schema"] = procedure.SchemaName.String - procedureMap["comment"] = procedure.Comment.String procedureMap["argument_types"] = procedureSignatureMap["argumentTypes"].([]string) procedureMap["return_type"] = procedureSignatureMap["returnType"].(string) - - procedures = append(procedures, procedureMap) + proceduresList = append(proceduresList, procedureMap) } d.SetId(fmt.Sprintf(`%v|%v`, databaseName, schemaName)) - return d.Set("procedures", procedures) + if err := d.Set("procedures", proceduresList); err != nil { + return diag.FromErr(err) + } + return nil } func parseArguments(arguments string) (map[string]interface{}, error) { diff --git a/pkg/datasources/procedures_acceptance_test.go b/pkg/datasources/procedures_acceptance_test.go index 48d407f2d7..55b955b1ee 100644 --- a/pkg/datasources/procedures_acceptance_test.go +++ b/pkg/datasources/procedures_acceptance_test.go @@ -1,83 +1,48 @@ package datasources_test import ( - "fmt" "strings" "testing" + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) func TestAcc_Procedures(t *testing.T) { - databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - procedureName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - procedureWithArgumentsName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - resource.ParallelTest(t, resource.TestCase{ - Providers: providers(), + procNameOne := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + procNameTwo := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + dataSourceName := "data.snowflake_procedures.procedures" + m := func() map[string]config.Variable { + return map[string]config.Variable{ + "database": config.StringVariable(acc.TestDatabaseName), + "schema": config.StringVariable(acc.TestSchemaName), + "proc_name_one": config.StringVariable(procNameOne), + "proc_name_two": config.StringVariable(procNameTwo), + } + } + configVariables := m() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, CheckDestroy: nil, Steps: []resource.TestStep{ { - Config: procedures(databaseName, schemaName, procedureName, procedureWithArgumentsName), + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_Procedures/complete"), + ConfigVariables: configVariables, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.snowflake_procedures.t", "database", databaseName), - resource.TestCheckResourceAttr("data.snowflake_procedures.t", "schema", schemaName), - resource.TestCheckResourceAttrSet("data.snowflake_procedures.t", "procedures.#"), - // resource.TestCheckResourceAttr("data.snowflake_procedures.t", "procedures.#", "3"), + resource.TestCheckResourceAttr(dataSourceName, "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr(dataSourceName, "schema", acc.TestSchemaName), + resource.TestCheckResourceAttrSet(dataSourceName, "procedures.#"), + // resource.TestCheckResourceAttr(dataSourceName, "procedures.#", "3"), // Extra 1 in procedure count above due to ASSOCIATE_SEMANTIC_CATEGORY_TAGS appearing in all "SHOW PROCEDURES IN ..." commands ), }, }, }) } - -func procedures(databaseName string, schemaName string, procedureName string, procedureWithArgumentsName string) string { - s := ` -resource "snowflake_database" "test_database" { - name = "%v" - comment = "Terraform acceptance test" -} - -resource "snowflake_schema" "test_schema" { - name = "%v" - database = snowflake_database.test_database.name - comment = "Terraform acceptance test" -} - -resource "snowflake_procedure" "test_proc_simple" { - name = "%v" - database = snowflake_database.test_database.name - schema = snowflake_schema.test_schema.name - return_type = "VARCHAR" - language = "JAVASCRIPT" - statement = <<-EOF - return "Hi" - EOF -} - -resource "snowflake_procedure" "test_proc" { - name = "%v" - database = snowflake_database.test_database.name - schema = snowflake_schema.test_schema.name - arguments { - name = "arg1" - type = "varchar" - } - comment = "Terraform acceptance test" - return_type = "varchar" - language = "JAVASCRIPT" - statement = <<-EOF - var X=1 - return X - EOF -} - -data snowflake_procedures "t" { - database = snowflake_database.test_database.name - schema = snowflake_schema.test_schema.name - depends_on = [snowflake_procedure.test_proc_simple, snowflake_procedure.test_proc] -} -` - return fmt.Sprintf(s, databaseName, schemaName, procedureName, procedureWithArgumentsName) -} diff --git a/pkg/datasources/testdata/TestAcc_Procedures/complete/test.tf b/pkg/datasources/testdata/TestAcc_Procedures/complete/test.tf new file mode 100644 index 0000000000..33ffb62f1d --- /dev/null +++ b/pkg/datasources/testdata/TestAcc_Procedures/complete/test.tf @@ -0,0 +1,49 @@ +variable "proc_name_one" { + type = string +} + +variable "proc_name_two" { + type = string +} + +variable "database" { + type = string +} + +variable "schema" { + type = string +} + +resource "snowflake_procedure" "test_proc_one" { + name = var.proc_name_one + database = var.database + schema = var.schema + return_type = "VARCHAR" + language = "JAVASCRIPT" + statement = <<-EOF + return "Hi" + EOF +} + +resource "snowflake_procedure" "test_proc_two" { + name = var.proc_name_two + database = var.database + schema = var.schema + arguments { + name = "arg1" + type = "varchar" + } + comment = "Terraform acceptance test" + return_type = "varchar" + language = "JAVASCRIPT" + statement = <<-EOF + var X=1 + return X + EOF +} + +data "snowflake_procedures" "procedures" { + database = var.database + schema = var.schema + depends_on = [snowflake_procedure.test_proc_one, snowflake_procedure.test_proc_two] +} diff --git a/pkg/resources/procedure.go b/pkg/resources/procedure.go index c395d2ecd8..9a9feaa314 100644 --- a/pkg/resources/procedure.go +++ b/pkg/resources/procedure.go @@ -1,20 +1,19 @@ package resources import ( + "context" "database/sql" - "errors" "fmt" - "log" + "regexp" "slices" "strings" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -var procedureLanguages = []string{"javascript", "java", "scala", "SQL", "python"} - var procedureSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -33,6 +32,12 @@ var procedureSchema = map[string]*schema.Schema{ 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{ @@ -53,7 +58,8 @@ var procedureSchema = map[string]*schema.Schema{ DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.EqualFold(old, new) }, - Description: "The argument type", + ValidateFunc: IsDataType(), + Description: "The argument type", }, }, }, @@ -82,8 +88,9 @@ var procedureSchema = map[string]*schema.Schema{ } return false }, - Required: true, - ForceNew: true, + ValidateFunc: IsDataType(), + Required: true, + ForceNew: true, }, "statement": { Type: schema.TypeString, @@ -99,7 +106,7 @@ var procedureSchema = map[string]*schema.Schema{ DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.EqualFold(old, new) }, - ValidateFunc: validation.StringInSlice(procedureLanguages, true), + ValidateFunc: validation.StringInSlice([]string{"javascript", "java", "scala", "SQL", "python"}, true), Description: "Specifies the language of the stored procedure code.", }, "execute_as": { @@ -124,6 +131,7 @@ var procedureSchema = map[string]*schema.Schema{ 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, @@ -163,17 +171,13 @@ var procedureSchema = map[string]*schema.Schema{ }, } -func DiffTypes(_, o, n string, _ *schema.ResourceData) bool { - return strings.EqualFold(strings.ToUpper(o), strings.ToUpper(n)) -} - // Procedure returns a pointer to the resource representing a stored procedure. func Procedure() *schema.Resource { return &schema.Resource{ - Create: CreateProcedure, - Read: ReadProcedure, - Update: UpdateProcedure, - Delete: DeleteProcedure, + CreateContext: CreateContextProcedure, + ReadContext: ReadContextProcedure, + UpdateContext: UpdateContextProcedure, + DeleteContext: DeleteContextProcedure, Schema: procedureSchema, Importer: &schema.ResourceImporter{ @@ -182,136 +186,369 @@ func Procedure() *schema.Resource { } } -// CreateProcedure implements schema.CreateFunc. -func CreateProcedure(d *schema.ResourceData, meta interface{}) error { +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 { db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) name := d.Get("name").(string) schema := d.Get("schema").(string) database := d.Get("database").(string) - s := d.Get("statement").(string) - ret := d.Get("return_type").(string) - - builder := snowflake.NewProcedureBuilder(database, schema, name, []string{}).WithStatement(s).WithReturnType(ret) - - // Set optionals, args - if _, ok := d.GetOk("arguments"); ok { - args := []map[string]string{} - for _, arg := range d.Get("arguments").([]interface{}) { - argDef := map[string]string{} - for key, val := range arg.(map[string]interface{}) { - argDef[key] = val.(string) - } - args = append(args, argDef) + 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)) } - builder.WithArgs(args) + } + 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) } - // Set optionals, default is false - if v, ok := d.GetOk("return_behavior"); ok { - builder.WithReturnBehavior(v.(string)) + 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) +} - // Set optionals, default is false - if v, ok := d.GetOk("null_input_behavior"); ok { - builder.WithNullInputBehavior(v.(string)) +func createJavaScriptProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + 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) } - // Set optionals, default is OWNER + // read optional params if v, ok := d.GetOk("execute_as"); ok { - builder.WithExecuteAs(v.(string)) + 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)) + } } - - // Set optionals, default is SQL - if v, ok := d.GetOk("language"); ok { - builder.WithLanguage(strings.ToUpper(v.(string))) + 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))) } - // Set optionals, runtime version for Python - if v, ok := d.GetOk("runtime_version"); ok { - builder.WithRuntimeVersion(v.(string)) + 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) +} - if v, ok := d.GetOk("comment"); ok { - builder.WithComment(v.(string)) +func createScalaProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + 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) } - // Set optionals, packages for Java / Python - if _, ok := d.GetOk("packages"); ok { - packages := []string{} - for _, pack := range d.Get("packages").([]interface{}) { - packages = append(packages, pack.(string)) + // 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)) } - builder.WithPackages(packages) } - - // Set optionals, imports for Java / Python + 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 := []string{} - for _, imp := range d.Get("imports").([]interface{}) { - imports = append(imports, imp.(string)) + imports := []sdk.ProcedureImportRequest{} + for _, item := range d.Get("imports").([]interface{}) { + imports = append(imports, *sdk.NewProcedureImportRequest(item.(string))) } - builder.WithImports(imports) + 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 { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) - // handler for Java / Python - if v, ok := d.GetOk("handler"); ok { - builder.WithHandler(v.(string)) + 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) } - q, err := builder.Create() - if err != nil { - return err + // 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 { + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + name := d.Get("name").(string) + schema := d.Get("schema").(string) + database := d.Get("database").(string) + id := sdk.NewSchemaObjectIdentifier(database, schema, name) - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error creating procedure %v err = %w", name, err) + 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) } - procedureID := &procedureID{ - DatabaseName: database, - SchemaName: schema, - ProcedureName: name, - ArgTypes: builder.ArgTypes(), + // 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)) + } } - d.SetId(procedureID.String()) + // [ { 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) + } - return ReadProcedure(d, meta) + 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) } -// ReadProcedure implements schema.ReadFunc. -func ReadProcedure(d *schema.ResourceData, meta interface{}) error { +func ReadContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + diags := diag.Diagnostics{} db := meta.(*sql.DB) - procedureID, err := splitProcedureID(d.Id()) - if err != nil { - return err - } - proc := snowflake.NewProcedureBuilder( - procedureID.DatabaseName, - procedureID.SchemaName, - procedureID.ProcedureName, - procedureID.ArgTypes, - ) + client := sdk.NewClientFromDB(db) - // some attributes can be retrieved only by Describe and some only by Show - stmt, err := proc.Describe() - if err != nil { - return err + id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) + if err := d.Set("name", id.Name()); err != nil { + return diag.FromErr(err) } - rows, err := snowflake.Query(db, stmt) - if err != nil { - return 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) } - defer rows.Close() - descPropValues, err := snowflake.ScanProcedureDescription(rows) + 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 { - return err + // 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 descPropValues { - switch desc.Property.String { + 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.String, "(", ""), ")", "") + args := strings.ReplaceAll(strings.ReplaceAll(desc.Value, "(", ""), ")", "") if args != "" { // Do nothing for functions without arguments argPairs := strings.Split(args, ", ") @@ -327,237 +564,218 @@ func ReadProcedure(d *schema.ResourceData, meta interface{}) error { } if err := d.Set("arguments", args); err != nil { - return err + return diag.FromErr(err) } } case "null handling": - if err := d.Set("null_input_behavior", desc.Value.String); err != nil { - return err - } - case "volatility": - if err := d.Set("return_behavior", desc.Value.String); err != nil { - return err + if err := d.Set("null_input_behavior", desc.Value); err != nil { + return diag.FromErr(err) } case "body": - if err := d.Set("statement", desc.Value.String); err != nil { - return err + if err := d.Set("statement", desc.Value); err != nil { + return diag.FromErr(err) } case "execute as": - if err := d.Set("execute_as", desc.Value.String); err != nil { - return err + if err := d.Set("execute_as", desc.Value); err != nil { + return diag.FromErr(err) } case "returns": - if err := d.Set("return_type", desc.Value.String); err != nil { - return err + if err := d.Set("return_type", desc.Value); err != nil { + return diag.FromErr(err) } case "language": - if err := d.Set("language", desc.Value.String); err != nil { - return err + if err := d.Set("language", desc.Value); err != nil { + return diag.FromErr(err) } case "runtime_version": - if err := d.Set("runtime_version", desc.Value.String); err != nil { - return err + 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.String, "[", ""), "]", ""), "'", "") + 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 err + return diag.FromErr(err) } } case "imports": - importsString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value.String, "[", ""), "]", ""), "'", ""), " ", "") + 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 err + return diag.FromErr(err) } } case "handler": - if err := d.Set("handler", desc.Value.String); err != nil { - return err + if err := d.Set("handler", desc.Value); err != nil { + return diag.FromErr(err) } - default: - log.Printf("[WARN] unexpected procedure property %v returned from Snowflake", desc.Property.String) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Unexpected procedure property returned from Snowflake", + Detail: fmt.Sprintf("Unexpected procedure property %v returned from Snowflake", desc.Property), + }) } } - q := proc.Show() - showRows, err := snowflake.Query(db, q) - if errors.Is(err, sql.ErrNoRows) { - // If not found, mark resource to be removed from state file during apply or refresh - log.Printf("[DEBUG] procedure (%s) not found", d.Id()) - d.SetId("") - return nil - } - if err != nil { - return err - } - defer showRows.Close() + request := sdk.NewShowProcedureRequest().WithIn(&sdk.In{Schema: sdk.NewDatabaseObjectIdentifier(id.DatabaseName(), id.SchemaName())}).WithLike(&sdk.Like{Pattern: sdk.String(id.Name())}) - foundProcedures, err := snowflake.ScanProcedures(showRows) + procedures, err := client.Procedures.Show(ctx, request) if err != nil { - return err + return diag.FromErr(err) } - // procedure names can be overloaded with different argument types so we - // iterate over and find the correct one - argSig, _ := proc.ArgumentsSignature() - - for _, v := range foundProcedures { - showArgs := strings.Split(v.Arguments.String, " RETURN ") - if showArgs[0] == argSig { - if err := d.Set("name", v.Name.String); err != nil { - return err - } - database := strings.Trim(v.DatabaseName.String, "\"") - if err := d.Set("database", database); err != nil { - return err - } - schema := strings.Trim(v.SchemaName.String, "\"") - if err := d.Set("schema", schema); err != nil { - return 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", v.Comment.String); err != nil { - return err + if err := d.Set("comment", procedure.Description); err != nil { + return diag.FromErr(err) } } } - return nil + return diags } -// UpdateProcedure implements schema.UpdateProcedure. -func UpdateProcedure(d *schema.ResourceData, meta interface{}) error { - pID, err := splitProcedureID(d.Id()) - if err != nil { - return err - } - builder := snowflake.NewProcedureBuilder( - pID.DatabaseName, - pID.SchemaName, - pID.ProcedureName, - pID.ArgTypes, - ) - +func UpdateContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + + id := sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(d.Id()) if d.HasChange("name") { name := d.Get("name") - q, err := builder.Rename(name.(string)) + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithRenameTo(sdk.Pointer(sdk.NewSchemaObjectIdentifier(id.DatabaseName(), id.SchemaName(), name.(string))))) if err != nil { - return err + return diag.FromErr(err) } - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error renaming procedure %v", d.Id()) + id = sdk.NewSchemaObjectIdentifierWithArguments(id.DatabaseName(), id.SchemaName(), name.(string), id.Arguments()) + if err := d.Set("name", name); err != nil { + return diag.FromErr(err) } - newID := &procedureID{ - DatabaseName: pID.DatabaseName, - SchemaName: pID.SchemaName, - ProcedureName: name.(string), - ArgTypes: pID.ArgTypes, - } - d.SetId(newID.String()) } - if d.HasChange("comment") { comment := d.Get("comment") - - if c := comment.(string); c == "" { - q, err := builder.RemoveComment() - if err != nil { - return err - } - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error unsetting comment for procedure %v", d.Id()) + 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 { - q, err := builder.ChangeComment(c) - if err != nil { - return err - } - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error updating comment for procedure %v", d.Id()) + 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") { executeAs := d.Get("execute_as") - - q, err := builder.ChangeExecuteAs(executeAs.(string)) - if err != nil { - return err - } - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error changing execute as for procedure %v", d.Id()) + if err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id.WithoutArguments(), id.Arguments()).WithExecuteAs(sdk.Pointer(sdk.ExecuteAs(executeAs.(string))))); err != nil { + return diag.FromErr(err) } } - - return ReadProcedure(d, meta) + return ReadContextProcedure(ctx, d, meta) } -// DeleteProcedure implements schema.DeleteFunc. -func DeleteProcedure(d *schema.ResourceData, meta interface{}) error { +func DeleteContextProcedure(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { db := meta.(*sql.DB) - pID, err := splitProcedureID(d.Id()) - if err != nil { - return err - } - builder := snowflake.NewProcedureBuilder( - pID.DatabaseName, - pID.SchemaName, - pID.ProcedureName, - pID.ArgTypes, - ) + client := sdk.NewClientFromDB(db) - q, err := builder.Drop() - if err != nil { - return err + 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 +} - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error deleting procedure %v err = %w", d.Id(), err) +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 +} - d.SetId("") - - return 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 } -type procedureID struct { - DatabaseName string - SchemaName string - ProcedureName string - ArgTypes []string +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 } -// splitProcedureID takes the ||| ID and returns -// the procedureID struct, for example MYDB|PUBLIC|PROC1|VARCHAR-DATE-VARCHAR -// returns struct -// -// DatabaseName: MYDB -// SchemaName: PUBLIC -// ProcedureName: PROC1 -// ArgTypes: [VARCHAR, DATE, VARCHAR] -func splitProcedureID(v string) (*procedureID, error) { - arr := strings.Split(v, "|") - if len(arr) != 4 { - return nil, fmt.Errorf("ID %v is invalid", v) - } - - return &procedureID{ - DatabaseName: arr[0], - SchemaName: arr[1], - ProcedureName: arr[2], - ArgTypes: strings.Split(arr[3], "-"), - }, 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 } -// the opposite of splitProcedureID. -func (pi *procedureID) String() string { - return fmt.Sprintf("%v|%v|%v|%v", - pi.DatabaseName, - pi.SchemaName, - pi.ProcedureName, - strings.Join(pi.ArgTypes, "-")) +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 1ccddc0dd5..92dabe3b33 100644 --- a/pkg/resources/procedure_acceptance_test.go +++ b/pkg/resources/procedure_acceptance_test.go @@ -1,141 +1,179 @@ package resources_test import ( - "fmt" - "os" "strings" "testing" acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestAcc_Procedure(t *testing.T) { - if _, ok := os.LookupEnv("SKIP_PROCEDURE_TESTS"); ok { - t.Skip("Skipping TestAcc_Procedure") - } +func testAccProcedure(t *testing.T, configDirectory string) { + t.Helper() - procName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) - expBody1 := "return \"Hi\"\n" - expBody2 := "var X=3\nreturn X\n" - expBody3 := "var X=1\nreturn X\n" + name := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + 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"), + } + } + variableSet2 := m() + variableSet2["comment"] = config.StringVariable("Terraform acceptance test - updated") resource.Test(t, resource.TestCase{ - Providers: acc.TestAccProviders(), - PreCheck: func() { acc.TestAccPreCheck(t) }, - CheckDestroy: nil, + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: testAccCheckDynamicTableDestroy, Steps: []resource.TestStep{ { - Config: procedureConfig(procName, acc.TestDatabaseName, acc.TestSchemaName), + ConfigDirectory: acc.ConfigurationDirectory(configDirectory), + ConfigVariables: m(), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "name", procName), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "comment", "Terraform acceptance test"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "statement", expBody2), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "arguments.#", "1"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "arguments.0.name", "ARG1"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "arguments.0.type", "VARCHAR"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc", "execute_as", "OWNER"), - - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_simple", "name", procName), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_simple", "comment", "user-defined procedure"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_simple", "statement", expBody1), - - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "name", procName), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "comment", "Proc with 2 args"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "statement", expBody3), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "execute_as", "CALLER"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "arguments.#", "2"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "arguments.1.name", "ARG2"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "arguments.1.type", "DATE"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "return_behavior", "IMMUTABLE"), - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_complex", "null_input_behavior", "RETURNS NULL ON NULL INPUT"), - - resource.TestCheckResourceAttr("snowflake_procedure.test_proc_sql", "name", procName+"_sql"), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "database", acc.TestDatabaseName), + resource.TestCheckResourceAttr(resourceName, "schema", acc.TestSchemaName), + resource.TestCheckResourceAttr(resourceName, "comment", "Terraform acceptance test"), + + // 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(configDirectory), + 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 { - ResourceName: "snowflake_procedure.test_proc_complex", + ConfigDirectory: acc.ConfigurationDirectory(configDirectory), + ConfigVariables: variableSet2, + ResourceName: resourceName, ImportState: true, ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "null_input_behavior", + "return_behavior", + }, }, }, }) } -func procedureConfig(name string, databaseName string, schemaName string) string { - return fmt.Sprintf(` - resource "snowflake_procedure" "test_proc_simple" { - name = "%s" - database = "%s" - schema = "%s" - return_type = "varchar" - language = "javascript" - statement = <<-EOF - return "Hi" - EOF - } +func TestAcc_Procedure_SQL(t *testing.T) { + testAccProcedure(t, "TestAcc_Procedure/sql") +} - resource "snowflake_procedure" "test_proc" { - name = "%s" - database = "%s" - schema = "%s" - arguments { - name = "arg1" - type = "varchar" - } - comment = "Terraform acceptance test" - language = "javascript" - return_type = "varchar" - statement = <<-EOF - var X=3 - return X - EOF - } +/* +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") +} +*/ - resource "snowflake_procedure" "test_proc_complex" { - name = "%s" - database = "%s" - schema = "%s" - arguments { - name = "arg1" - type = "varchar" - } - arguments { - name = "arg2" - type = "DATE" +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 := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)) + 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"), } - comment = "Proc with 2 args" - return_type = "VARCHAR" - execute_as = "CALLER" - return_behavior = "IMMUTABLE" - null_input_behavior = "RETURNS NULL ON NULL INPUT" - language = "javascript" - statement = <<-EOF - var X=1 - return X - EOF } + variableSet2 := m() + variableSet2["comment"] = config.StringVariable("Terraform acceptance test - updated") - resource "snowflake_procedure" "test_proc_sql" { - name = "%s_sql" - database = "%s" - schema = "%s" - language = "SQL" - return_type = "INTEGER" - execute_as = "CALLER" - return_behavior = "IMMUTABLE" - null_input_behavior = "RETURNS NULL ON NULL INPUT" - statement = <(, , ..) -func (pb *ProcedureBuilder) ArgumentsSignature() (string, error) { - return fmt.Sprintf(`%v(%v)`, strings.ToUpper(pb.name), strings.ToUpper(strings.Join(pb.argumentTypes, ", "))), nil -} - -// WithArgs sets the args and argumentTypes on the ProcedureBuilder. -func (pb *ProcedureBuilder) WithArgs(args []map[string]string) *ProcedureBuilder { - pb.args = []map[string]string{} - for _, arg := range args { - argName := arg["name"] - argType := strings.ToUpper(arg["type"]) - pb.args = append(pb.args, map[string]string{"name": argName, "type": argType}) - pb.argumentTypes = append(pb.argumentTypes, argType) - } - return pb -} - -// WithReturnBehavior. -func (pb *ProcedureBuilder) WithReturnBehavior(s string) *ProcedureBuilder { - pb.returnBehavior = s - return pb -} - -// WithNullInputBehavior. -func (pb *ProcedureBuilder) WithNullInputBehavior(s string) *ProcedureBuilder { - pb.nullInputBehavior = s - return pb -} - -// WithReturnType adds the data type of the return type to the ProcedureBuilder. -func (pb *ProcedureBuilder) WithReturnType(s string) *ProcedureBuilder { - pb.returnType = strings.ToUpper(s) - return pb -} - -// WithExecuteAs sets the execute to OWNER or CALLER. -func (pb *ProcedureBuilder) WithExecuteAs(s string) *ProcedureBuilder { - pb.executeAs = s - return pb -} - -// WithLanguage sets the language to SQL, JAVA, SCALA or JAVASCRIPT. -func (pb *ProcedureBuilder) WithLanguage(s string) *ProcedureBuilder { - pb.language = s - return pb -} - -// WithRuntimeVersion. -func (pb *ProcedureBuilder) WithRuntimeVersion(r string) *ProcedureBuilder { - pb.runtimeVersion = r - return pb -} - -// WithPackages. -func (pb *ProcedureBuilder) WithPackages(s []string) *ProcedureBuilder { - pb.packages = s - return pb -} - -// WithImports adds jar files to import for Java function or Python file for Python function. -func (pb *ProcedureBuilder) WithImports(s []string) *ProcedureBuilder { - pb.imports = s - return pb -} - -// WithHandler sets the handler method for Java / Python function. -func (pb *ProcedureBuilder) WithHandler(s string) *ProcedureBuilder { - pb.handler = s - return pb -} - -// WithComment adds a comment to the ProcedureBuilder. -func (pb *ProcedureBuilder) WithComment(c string) *ProcedureBuilder { - pb.comment = c - return pb -} - -// WithStatement adds the SQL statement to be used for the procedure. -func (pb *ProcedureBuilder) WithStatement(s string) *ProcedureBuilder { - pb.statement = s - return pb -} - -// Returns the argument types. -func (pb *ProcedureBuilder) ArgTypes() []string { - return pb.argumentTypes -} - -// Procedure returns a pointer to a Builder that abstracts the DDL operations for a stored procedure. -// -// Supported DDL operations are: -// - CREATE PROCEDURE -// - ALTER PROCEDURE -// - DROP PROCEDURE -// - SHOW PROCEDURE -// - DESCRIBE -// -// [Snowflake Reference](https://docs.snowflake.com/en/sql-reference/stored-procedures.html) -func NewProcedureBuilder(db, schema, name string, argTypes []string) *ProcedureBuilder { - return &ProcedureBuilder{ - name: name, - db: db, - schema: schema, - argumentTypes: argTypes, - } -} - -// Create returns the SQL query that will create a new procedure. -func (pb *ProcedureBuilder) Create() (string, error) { - var q strings.Builder - - q.WriteString("CREATE OR REPLACE") - - qn, err := pb.QualifiedNameWithoutArguments() - if err != nil { - return "", err - } - - q.WriteString(fmt.Sprintf(" PROCEDURE %v", qn)) - - q.WriteString(`(`) - args := []string{} - for _, arg := range pb.args { - args = append(args, fmt.Sprintf(`%v %v`, EscapeString(arg["name"]), EscapeString(arg["type"]))) - } - q.WriteString(strings.Join(args, ", ")) - q.WriteString(`)`) - - q.WriteString(fmt.Sprintf(" RETURNS %v", pb.returnType)) - if pb.language != "" { - q.WriteString(fmt.Sprintf(" LANGUAGE %v", EscapeString(pb.language))) - } - if pb.nullInputBehavior != "" { - q.WriteString(fmt.Sprintf(` %v`, EscapeString(pb.nullInputBehavior))) - } - if pb.returnBehavior != "" { - q.WriteString(fmt.Sprintf(` %v`, EscapeString(pb.returnBehavior))) - } - if pb.runtimeVersion != "" { - q.WriteString(fmt.Sprintf(" RUNTIME_VERSION = '%v'", EscapeString(pb.runtimeVersion))) - } - if len(pb.packages) > 0 { - q.WriteString(` PACKAGES = (`) - packages := []string{} - for _, pack := range pb.packages { - packages = append(packages, fmt.Sprintf(`'%v'`, pack)) - } - q.WriteString(strings.Join(packages, ", ")) - q.WriteString(`)`) - } - if len(pb.imports) > 0 { - q.WriteString(` IMPORTS = (`) - imports := []string{} - for _, imp := range pb.imports { - imports = append(imports, fmt.Sprintf(`'%v'`, imp)) - } - q.WriteString(strings.Join(imports, ", ")) - q.WriteString(`)`) - } - if pb.handler != "" { - q.WriteString(fmt.Sprintf(" HANDLER = '%v'", pb.handler)) - } - if pb.comment != "" { - q.WriteString(fmt.Sprintf(" COMMENT = '%v'", EscapeString(pb.comment))) - } - q.WriteString(fmt.Sprintf(" EXECUTE AS %v", pb.executeAs)) - q.WriteString(fmt.Sprintf(" AS $$%v$$", pb.statement)) - return q.String(), nil -} - -// Rename returns the SQL query that will rename the procedure. -func (pb *ProcedureBuilder) Rename(newName string) (string, error) { - oldName, err := pb.QualifiedName() - if err != nil { - return "", err - } - pb.name = newName - - qn, err := pb.QualifiedNameWithoutArguments() - if err != nil { - return "", err - } - return fmt.Sprintf(`ALTER PROCEDURE %v RENAME TO %v`, oldName, qn), nil -} - -// ChangeComment returns the SQL query that will update the comment on the procedure. -func (pb *ProcedureBuilder) ChangeComment(c string) (string, error) { - qn, err := pb.QualifiedName() - if err != nil { - return "", err - } - - return fmt.Sprintf(`ALTER PROCEDURE %v SET COMMENT = '%v'`, qn, EscapeString(c)), nil -} - -// RemoveComment returns the SQL query that will remove the comment on the procedure. -func (pb *ProcedureBuilder) RemoveComment() (string, error) { - qn, err := pb.QualifiedName() - if err != nil { - return "", err - } - return fmt.Sprintf(`ALTER PROCEDURE %v UNSET COMMENT`, qn), nil -} - -// ChangeExecuteAs returns the SQL query that will update the call mode on the procedure. -func (pb *ProcedureBuilder) ChangeExecuteAs(c string) (string, error) { - qn, err := pb.QualifiedName() - if err != nil { - return "", err - } - return fmt.Sprintf(`ALTER PROCEDURE %v EXECUTE AS %v`, qn, c), nil -} - -// Show returns the SQL query that will show the row representing this procedure. -// This show statement returns all procedures with the given name (overloaded ones). -func (pb *ProcedureBuilder) Show() string { - return fmt.Sprintf(`SHOW PROCEDURES LIKE '%v' IN SCHEMA "%v"."%v"`, pb.name, pb.db, pb.schema) -} - -// To describe the procedure the name must be specified as fully qualified name -// including argument types. -func (pb *ProcedureBuilder) Describe() (string, error) { - qn, err := pb.QualifiedName() - if err != nil { - return "", err - } - return fmt.Sprintf(`DESCRIBE PROCEDURE %v`, qn), nil -} - -// Drop returns the SQL query that will drop the procedure. -func (pb *ProcedureBuilder) Drop() (string, error) { - qn, err := pb.QualifiedName() - if err != nil { - return "", err - } - return fmt.Sprintf(`DROP PROCEDURE %v`, qn), nil -} - -type Procedure struct { - Comment sql.NullString `db:"description"` - // Snowflake returns is_secure in the show procedure output, but it is irrelevant - Name sql.NullString `db:"name"` - SchemaName sql.NullString `db:"schema_name"` - Text sql.NullString `db:"text"` - DatabaseName sql.NullString `db:"catalog_name"` - Arguments sql.NullString `db:"arguments"` -} - -type ProcedureDescription struct { - Property sql.NullString `db:"property"` - Value sql.NullString `db:"value"` -} - -// ScanProcedureDescription reads through the rows with property and value columns -// and returns a slice of procedureDescription structs. -func ScanProcedureDescription(rows *sqlx.Rows) ([]ProcedureDescription, error) { - pdsl := []ProcedureDescription{} - for rows.Next() { - pd := ProcedureDescription{} - err := rows.StructScan(&pd) - if err != nil { - return nil, err - } - pdsl = append(pdsl, pd) - } - return pdsl, rows.Err() -} - -// SHOW PROCEDURE can return more than one item because of procedure names overloading -// https://docs.snowflake.com/en/sql-reference/sql/show-procedures.html -func ScanProcedures(rows *sqlx.Rows) ([]*Procedure, error) { - var pcs []*Procedure - for rows.Next() { - r := &Procedure{} - err := rows.StructScan(r) - if err != nil { - return nil, err - } - pcs = append(pcs, r) - } - return pcs, rows.Err() -} - -func ListProcedures(databaseName string, schemaName string, db *sql.DB) ([]Procedure, error) { - stmt := fmt.Sprintf(`SHOW PROCEDURES IN SCHEMA "%s"."%v"`, databaseName, schemaName) - rows, err := Query(db, stmt) - if err != nil { - return nil, err - } - defer rows.Close() - - dbs := []Procedure{} - if err := sqlx.StructScan(rows, &dbs); err != nil { - if errors.Is(err, sql.ErrNoRows) { - log.Println("[DEBUG] no procedures found") - return nil, nil - } - return nil, fmt.Errorf("unable to scan row for %s err = %w", stmt, err) - } - return dbs, nil -} diff --git a/pkg/snowflake/procedure_test.go b/pkg/snowflake/procedure_test.go deleted file mode 100644 index b22e9db98b..0000000000 --- a/pkg/snowflake/procedure_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package snowflake - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func getProcedure(withArgs bool) *ProcedureBuilder { - s := NewProcedureBuilder("test_db", "test_schema", "test_proc", []string{}) - s.WithStatement(`var message = "Hi"` + "\n" + `return message`) - s.WithReturnType("varchar") - s.WithExecuteAs("CALLER") - if withArgs { - s.WithArgs([]map[string]string{ - {"name": "user", "type": "varchar"}, - {"name": "eventdt", "type": "date"}, - }) - } - return s -} - -func TestProcedureQualifiedName(t *testing.T) { - r := require.New(t) - s := getProcedure(true) - qn, _ := s.QualifiedName() - r.Equal(`"test_db"."test_schema"."test_proc"(VARCHAR, DATE)`, qn) - qna, _ := s.QualifiedNameWithoutArguments() - r.Equal(`"test_db"."test_schema"."test_proc"`, qna) -} - -func TestProcedureCreate(t *testing.T) { - r := require.New(t) - s := getProcedure(true) - - r.Equal([]string{"VARCHAR", "DATE"}, s.ArgTypes()) - createStmnt, _ := s.Create() - expected := `CREATE OR REPLACE PROCEDURE "test_db"."test_schema"."test_proc"` + - `(user VARCHAR, eventdt DATE) RETURNS VARCHAR EXECUTE AS CALLER AS $$` + - `var message = "Hi"` + "\nreturn message$$" - r.Equal(expected, createStmnt) -} - -func TestProcedureCreateWithOptionalParams(t *testing.T) { - r := require.New(t) - s := getProcedure(true) - s.WithNullInputBehavior("RETURNS NULL ON NULL INPUT") - s.WithReturnBehavior("IMMUTABLE") - s.WithLanguage("PYTHON") - s.WithRuntimeVersion("3.8") - s.WithPackages([]string{"snowflake-snowpark-python", "pandas"}) - s.WithImports([]string{"@\"test_db\".\"test_schema\".\"test_stage\"/handler.py"}) - s.WithHandler("handler.test") - s.WithComment("this is cool proc!") - createStmnt, _ := s.Create() - expected := `CREATE OR REPLACE PROCEDURE "test_db"."test_schema"."test_proc"` + - `(user VARCHAR, eventdt DATE) RETURNS VARCHAR LANGUAGE PYTHON RETURNS NULL ON NULL INPUT ` + - `IMMUTABLE RUNTIME_VERSION = '3.8' PACKAGES = ('snowflake-snowpark-python', 'pandas') ` + - `IMPORTS = ('@"test_db"."test_schema"."test_stage"/handler.py') HANDLER = 'handler.test' ` + - `COMMENT = 'this is cool proc!' EXECUTE AS CALLER AS $$var message = "Hi"` + "\nreturn message$$" - r.Equal(expected, createStmnt) -} - -func TestProcedureDrop(t *testing.T) { - r := require.New(t) - - // Without arg - s := getProcedure(false) - stmnt, _ := s.Drop() - r.Equal(`DROP PROCEDURE "test_db"."test_schema"."test_proc"()`, stmnt) - - // With arg - ss := getProcedure(true) - stmnt, _ = ss.Drop() - r.Equal(`DROP PROCEDURE "test_db"."test_schema"."test_proc"(VARCHAR, DATE)`, stmnt) -} - -func TestProcedureShow(t *testing.T) { - r := require.New(t) - s := getProcedure(false) - stmnt := s.Show() - r.Equal(`SHOW PROCEDURES LIKE 'test_proc' IN SCHEMA "test_db"."test_schema"`, stmnt) -} - -func TestProcedureRename(t *testing.T) { - r := require.New(t) - s := getProcedure(false) - - stmnt, _ := s.Rename("new_proc") - expected := `ALTER PROCEDURE "test_db"."test_schema"."test_proc"() RENAME TO "test_db"."test_schema"."new_proc"` - r.Equal(expected, stmnt) -} - -func TestProcedureChangeComment(t *testing.T) { - r := require.New(t) - s := getProcedure(true) - - stmnt, _ := s.ChangeComment("not used") - expected := `ALTER PROCEDURE "test_db"."test_schema"."test_proc"(VARCHAR, DATE) SET COMMENT = 'not used'` - r.Equal(expected, stmnt) -} - -func TestProcedureRemoveComment(t *testing.T) { - r := require.New(t) - s := getProcedure(false) - - stmnt, _ := s.RemoveComment() - expected := `ALTER PROCEDURE "test_db"."test_schema"."test_proc"() UNSET COMMENT` - r.Equal(expected, stmnt) -} - -func TestProcedureChangeExecuteAs(t *testing.T) { - r := require.New(t) - s := getProcedure(false) - - stmnt, _ := s.ChangeExecuteAs("OWNER") - expected := `ALTER PROCEDURE "test_db"."test_schema"."test_proc"() EXECUTE AS OWNER` - r.Equal(expected, stmnt) -} - -func TestProcedureDescribe(t *testing.T) { - r := require.New(t) - s := getProcedure(false) - - stmnt, _ := s.Describe() - expected := `DESCRIBE PROCEDURE "test_db"."test_schema"."test_proc"()` - r.Equal(expected, stmnt) -} - -func TestProcedureArgumentsSignature(t *testing.T) { - r := require.New(t) - s := getProcedure(false) - sign, _ := s.ArgumentsSignature() - r.Equal("TEST_PROC()", sign) - s = getProcedure(true) - sign, _ = s.ArgumentsSignature() - r.Equal("TEST_PROC(VARCHAR, DATE)", sign) -}