diff --git a/docs/resources/function_sql.md b/docs/resources/function_sql.md index 4a48191740..bb4e772742 100644 --- a/docs/resources/function_sql.md +++ b/docs/resources/function_sql.md @@ -30,7 +30,6 @@ Resource used to manage sql function objects. For more information, check [funct - `is_secure` (String) Specifies that the function is secure. By design, the Snowflake's `SHOW FUNCTIONS` command does not provide information about secure functions (consult [function docs](https://docs.snowflake.com/en/sql-reference/sql/create-function#id1) and [Protecting Sensitive Information with Secure UDFs and Stored Procedures](https://docs.snowflake.com/en/developer-guide/secure-udf-procedure)) which is essential to manage/import function with Terraform. Use the role owning the function while managing secure functions. Available options are: "true" or "false". When the value is not set in the configuration the provider will put "default" there which means to use the Snowflake default for this value. - `log_level` (String) LOG_LEVEL to use when filtering events For more information, check [LOG_LEVEL docs](https://docs.snowflake.com/en/sql-reference/parameters#log-level). - `metric_level` (String) METRIC_LEVEL value to control whether to emit metrics to Event Table For more information, check [METRIC_LEVEL docs](https://docs.snowflake.com/en/sql-reference/parameters#metric-level). -- `null_input_behavior` (String) Specifies the behavior of the function when called with null inputs. Valid values are (case-insensitive): `CALLED ON NULL INPUT` | `RETURNS NULL ON NULL INPUT`. - `return_results_behavior` (String) Specifies the behavior of the function when returning results. Valid values are (case-insensitive): `VOLATILE` | `IMMUTABLE`. - `trace_level` (String) Trace level value to use when generating/filtering trace events For more information, check [TRACE_LEVEL docs](https://docs.snowflake.com/en/sql-reference/parameters#trace-level). diff --git a/pkg/resources/function_commons.go b/pkg/resources/function_commons.go index 7dddd097e7..12ea55bd73 100644 --- a/pkg/resources/function_commons.go +++ b/pkg/resources/function_commons.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "log" + "reflect" "slices" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -86,7 +88,6 @@ var ( "is_secure", "arguments", "return_type", - "null_input_behavior", "return_results_behavior", "comment", "function_definition", @@ -98,6 +99,7 @@ var ( javaFunctionSchemaDefinition = functionSchemaDef{ additionalArguments: []string{ "runtime_version", + "null_input_behavior", "imports", "packages", "handler", @@ -115,7 +117,9 @@ var ( targetPathDescription: "The TARGET_PATH clause specifies the location to which Snowflake should write the compiled code (JAR file) after compiling the source code specified in the `function_definition`. If this clause is included, the user should manually remove the JAR file when it is no longer needed (typically when the Java UDF is dropped). If this clause is omitted, Snowflake re-compiles the source code each time the code is needed. The JAR file is not stored permanently, and the user does not need to clean up the JAR file. Snowflake returns an error if the TARGET_PATH matches an existing file; you cannot use TARGET_PATH to overwrite an existing file.", } javascriptFunctionSchemaDefinition = functionSchemaDef{ - additionalArguments: []string{}, + additionalArguments: []string{ + "null_input_behavior", + }, functionDefinitionDescription: functionDefinitionTemplate("JavaScript", "https://docs.snowflake.com/en/developer-guide/udf/javascript/udf-javascript-introduction"), functionDefinitionRequired: true, } @@ -123,6 +127,7 @@ var ( additionalArguments: []string{ "is_aggregate", "runtime_version", + "null_input_behavior", "imports", "packages", "handler", @@ -139,6 +144,7 @@ var ( scalaFunctionSchemaDefinition = functionSchemaDef{ additionalArguments: []string{ "runtime_version", + "null_input_behavior", "imports", "packages", "handler", @@ -405,6 +411,106 @@ func DeleteFunction(ctx context.Context, d *schema.ResourceData, meta any) diag. return nil } +func UpdateFunction(language string, readFunc func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics) func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + newId := sdk.NewSchemaObjectIdentifierWithArgumentsInSchema(id.SchemaId(), d.Get("name").(string), id.ArgumentDataTypes()...) + + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithRenameTo(newId.SchemaObjectId())) + if err != nil { + return diag.FromErr(fmt.Errorf("error renaming function %v err = %w", d.Id(), err)) + } + + d.SetId(helpers.EncodeResourceIdentifier(newId)) + id = newId + } + + // Batch SET operations and UNSET operations + setRequest := sdk.NewFunctionSetRequest() + unsetRequest := sdk.NewFunctionUnsetRequest() + + _ = stringAttributeUpdate(d, "comment", &setRequest.Comment, &unsetRequest.Comment) + + switch language { + case "JAVA", "SCALA", "PYTHON": + err = errors.Join( + func() error { + if d.HasChange("secrets") { + return setSecretsInBuilder(d, func(references []sdk.SecretReference) *sdk.FunctionSetRequest { + return setRequest.WithSecretsList(sdk.SecretsListRequest{SecretsList: references}) + }) + } + return nil + }(), + func() error { + if d.HasChange("external_access_integrations") { + return setExternalAccessIntegrationsInBuilder(d, func(references []sdk.AccountObjectIdentifier) any { + if len(references) == 0 { + return unsetRequest.WithExternalAccessIntegrations(true) + } else { + return setRequest.WithExternalAccessIntegrations(references) + } + }) + } + return nil + }(), + ) + if err != nil { + return diag.FromErr(err) + } + } + + if updateParamDiags := handleFunctionParametersUpdate(d, setRequest, unsetRequest); len(updateParamDiags) > 0 { + return updateParamDiags + } + + // Apply SET and UNSET changes + if !reflect.DeepEqual(*setRequest, *sdk.NewFunctionSetRequest()) { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSet(*setRequest)) + if err != nil { + d.Partial(true) + return diag.FromErr(err) + } + } + if !reflect.DeepEqual(*unsetRequest, *sdk.NewFunctionUnsetRequest()) { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithUnset(*unsetRequest)) + if err != nil { + d.Partial(true) + return diag.FromErr(err) + } + } + + // has to be handled separately + if d.HasChange("is_secure") { + if v := d.Get("is_secure").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) + if err != nil { + return diag.FromErr(err) + } + err = client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSetSecure(parsed)) + if err != nil { + d.Partial(true) + return diag.FromErr(err) + } + } else { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithUnsetSecure(true)) + if err != nil { + d.Partial(true) + return diag.FromErr(err) + } + } + } + + return readFunc(ctx, d, meta) + } +} + // TODO [SNOW-1850370]: Make the rest of the functions in this file generic (for reuse with procedures) func parseFunctionArgumentsCommon(d *schema.ResourceData) ([]sdk.FunctionArgumentRequest, error) { args := make([]sdk.FunctionArgumentRequest, 0) diff --git a/pkg/resources/function_java.go b/pkg/resources/function_java.go index e085fc0c97..4dae73f94c 100644 --- a/pkg/resources/function_java.go +++ b/pkg/resources/function_java.go @@ -3,7 +3,6 @@ package resources import ( "context" "errors" - "fmt" "reflect" "strings" @@ -23,7 +22,7 @@ func FunctionJava() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.FunctionJava, CreateContextFunctionJava), ReadContext: TrackingReadWrapper(resources.FunctionJava, ReadContextFunctionJava), - UpdateContext: TrackingUpdateWrapper(resources.FunctionJava, UpdateContextFunctionJava), + UpdateContext: TrackingUpdateWrapper(resources.FunctionJava, UpdateFunction("JAVA", ReadContextFunctionJava)), DeleteContext: TrackingDeleteWrapper(resources.FunctionJava, DeleteFunction), Description: "Resource used to manage java function objects. For more information, check [function documentation](https://docs.snowflake.com/en/sql-reference/sql/create-function).", @@ -147,97 +146,3 @@ func ReadContextFunctionJava(ctx context.Context, d *schema.ResourceData, meta a return nil } - -func UpdateContextFunctionJava(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*provider.Context).Client - id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - if d.HasChange("name") { - newId := sdk.NewSchemaObjectIdentifierWithArgumentsInSchema(id.SchemaId(), d.Get("name").(string), id.ArgumentDataTypes()...) - - err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithRenameTo(newId.SchemaObjectId())) - if err != nil { - return diag.FromErr(fmt.Errorf("error renaming function %v err = %w", d.Id(), err)) - } - - d.SetId(helpers.EncodeResourceIdentifier(newId)) - id = newId - } - - // Batch SET operations and UNSET operations - setRequest := sdk.NewFunctionSetRequest() - unsetRequest := sdk.NewFunctionUnsetRequest() - - err = errors.Join( - stringAttributeUpdate(d, "comment", &setRequest.Comment, &unsetRequest.Comment), - func() error { - if d.HasChange("secrets") { - return setSecretsInBuilder(d, func(references []sdk.SecretReference) *sdk.FunctionSetRequest { - return setRequest.WithSecretsList(sdk.SecretsListRequest{SecretsList: references}) - }) - } - return nil - }(), - func() error { - if d.HasChange("external_access_integrations") { - return setExternalAccessIntegrationsInBuilder(d, func(references []sdk.AccountObjectIdentifier) any { - if len(references) == 0 { - return unsetRequest.WithExternalAccessIntegrations(true) - } else { - return setRequest.WithExternalAccessIntegrations(references) - } - }) - } - return nil - }(), - ) - if err != nil { - return diag.FromErr(err) - } - - if updateParamDiags := handleFunctionParametersUpdate(d, setRequest, unsetRequest); len(updateParamDiags) > 0 { - return updateParamDiags - } - - // Apply SET and UNSET changes - if !reflect.DeepEqual(*setRequest, *sdk.NewFunctionSetRequest()) { - err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSet(*setRequest)) - if err != nil { - d.Partial(true) - return diag.FromErr(err) - } - } - if !reflect.DeepEqual(*unsetRequest, *sdk.NewFunctionUnsetRequest()) { - err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithUnset(*unsetRequest)) - if err != nil { - d.Partial(true) - return diag.FromErr(err) - } - } - - // has to be handled separately - if d.HasChange("is_secure") { - if v := d.Get("is_secure").(string); v != BooleanDefault { - parsed, err := booleanStringToBool(v) - if err != nil { - return diag.FromErr(err) - } - err = client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSetSecure(parsed)) - if err != nil { - d.Partial(true) - return diag.FromErr(err) - } - } else { - err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithUnsetSecure(true)) - if err != nil { - d.Partial(true) - return diag.FromErr(err) - } - } - } - - return ReadContextFunctionJava(ctx, d, meta) -} diff --git a/pkg/resources/function_javascript.go b/pkg/resources/function_javascript.go index 0ba7e955b7..fe0884dd67 100644 --- a/pkg/resources/function_javascript.go +++ b/pkg/resources/function_javascript.go @@ -2,11 +2,17 @@ package resources import ( "context" + "errors" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +22,7 @@ func FunctionJavascript() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.FunctionJavascript, CreateContextFunctionJavascript), ReadContext: TrackingReadWrapper(resources.FunctionJavascript, ReadContextFunctionJavascript), - UpdateContext: TrackingUpdateWrapper(resources.FunctionJavascript, UpdateContextFunctionJavascript), + UpdateContext: TrackingUpdateWrapper(resources.FunctionJavascript, UpdateFunction("JAVASCRIPT", ReadContextFunctionJavascript)), DeleteContext: TrackingDeleteWrapper(resources.FunctionJavascript, DeleteFunction), Description: "Resource used to manage javascript function objects. For more information, check [function documentation](https://docs.snowflake.com/en/sql-reference/sql/create-function).", @@ -25,7 +31,11 @@ func FunctionJavascript() *schema.Resource { ComputedIfAnyAttributeChanged(javascriptFunctionSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(functionParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllFunctionParameters), strings.ToLower)...), functionParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only potential option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("function_language", "JAVASCRIPT"), )), Schema: collections.MergeMaps(javascriptFunctionSchema, functionParametersSchema), @@ -36,17 +46,90 @@ func FunctionJavascript() *schema.Resource { } func CreateContextFunctionJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseFunctionArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returns, err := parseFunctionReturnsCommon(d) + if err != nil { + return diag.FromErr(err) + } + functionDefinition := d.Get("function_definition").(string) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.FunctionArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForJavascriptFunctionRequestDefinitionWrapped(id.SchemaObjectId(), *returns, functionDefinition). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + attributeMappedValueCreateBuilder[string](d, "return_results_behavior", request.WithReturnResultsBehavior, sdk.ToReturnResultsBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Functions.CreateForJavascript(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create function (query does not fail but parameters stay unchanged) + setRequest := sdk.NewFunctionSetRequest() + if parametersCreateDiags := handleFunctionParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewFunctionSetRequest()) { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextFunctionJavascript(ctx, d, meta) } func ReadContextFunctionJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } -func UpdateContextFunctionJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + allFunctionDetails, diags := queryAllFunctionDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allFunctionDetails.functionDetails.NormalizedArguments), + d.Set("return_type", allFunctionDetails.functionDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + // not reading return_results_behavior on purpose (handled as external change to show output) + d.Set("comment", allFunctionDetails.function.Description), + setRequiredFromStringPtr(d, "handler", allFunctionDetails.functionDetails.Handler), + setOptionalFromStringPtr(d, "function_definition", allFunctionDetails.functionDetails.Body), + d.Set("function_language", allFunctionDetails.functionDetails.Language), + + handleFunctionParameterRead(d, allFunctionDetails.functionParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.FunctionToSchema(allFunctionDetails.function)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.FunctionParametersToSchema(allFunctionDetails.functionParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func DeleteContextFunctionJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/function_python.go b/pkg/resources/function_python.go index cc6c137aff..ebc3dd7259 100644 --- a/pkg/resources/function_python.go +++ b/pkg/resources/function_python.go @@ -2,11 +2,17 @@ package resources import ( "context" + "errors" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +22,7 @@ func FunctionPython() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.FunctionPython, CreateContextFunctionPython), ReadContext: TrackingReadWrapper(resources.FunctionPython, ReadContextFunctionPython), - UpdateContext: TrackingUpdateWrapper(resources.FunctionPython, UpdateContextFunctionPython), + UpdateContext: TrackingUpdateWrapper(resources.FunctionPython, UpdateFunction("PYTHON", ReadContextFunctionPython)), DeleteContext: TrackingDeleteWrapper(resources.FunctionPython, DeleteFunction), Description: "Resource used to manage python function objects. For more information, check [function documentation](https://docs.snowflake.com/en/sql-reference/sql/create-function).", @@ -25,7 +31,11 @@ func FunctionPython() *schema.Resource { ComputedIfAnyAttributeChanged(pythonFunctionSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(functionParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllFunctionParameters), strings.ToLower)...), functionParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only potential option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("function_language", "PYTHON"), )), Schema: collections.MergeMaps(pythonFunctionSchema, functionParametersSchema), @@ -36,17 +46,101 @@ func FunctionPython() *schema.Resource { } func CreateContextFunctionPython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseFunctionArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returns, err := parseFunctionReturnsCommon(d) + if err != nil { + return diag.FromErr(err) + } + handler := d.Get("handler").(string) + runtimeVersion := d.Get("runtime_version").(string) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.FunctionArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForPythonFunctionRequest(id.SchemaObjectId(), *returns, runtimeVersion, handler). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + attributeMappedValueCreateBuilder[string](d, "return_results_behavior", request.WithReturnResultsBehavior, sdk.ToReturnResultsBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + setFunctionImportsInBuilder(d, request.WithImports), + setFunctionPackagesInBuilder(d, request.WithPackages), + setExternalAccessIntegrationsInBuilder(d, request.WithExternalAccessIntegrations), + setSecretsInBuilder(d, request.WithSecrets), + stringAttributeCreateBuilder(d, "function_definition", request.WithFunctionDefinitionWrapped), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Functions.CreateForPython(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create function (query does not fail but parameters stay unchanged) + setRequest := sdk.NewFunctionSetRequest() + if parametersCreateDiags := handleFunctionParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewFunctionSetRequest()) { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextFunctionPython(ctx, d, meta) } func ReadContextFunctionPython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } -func UpdateContextFunctionPython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + allFunctionDetails, diags := queryAllFunctionDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allFunctionDetails.functionDetails.NormalizedArguments), + d.Set("return_type", allFunctionDetails.functionDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + // not reading return_results_behavior on purpose (handled as external change to show output) + setOptionalFromStringPtr(d, "runtime_version", allFunctionDetails.functionDetails.RuntimeVersion), + d.Set("comment", allFunctionDetails.function.Description), + readFunctionOrProcedureImports(d, allFunctionDetails.functionDetails.NormalizedImports), + d.Set("packages", allFunctionDetails.functionDetails.NormalizedPackages), + setRequiredFromStringPtr(d, "handler", allFunctionDetails.functionDetails.Handler), + readFunctionOrProcedureExternalAccessIntegrations(d, allFunctionDetails.functionDetails.NormalizedExternalAccessIntegrations), + readFunctionOrProcedureSecrets(d, allFunctionDetails.functionDetails.NormalizedSecrets), + setOptionalFromStringPtr(d, "function_definition", allFunctionDetails.functionDetails.Body), + d.Set("function_language", allFunctionDetails.functionDetails.Language), + + handleFunctionParameterRead(d, allFunctionDetails.functionParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.FunctionToSchema(allFunctionDetails.function)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.FunctionParametersToSchema(allFunctionDetails.functionParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func DeleteContextFunctionPython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/function_scala.go b/pkg/resources/function_scala.go index ff2bded481..491af794b4 100644 --- a/pkg/resources/function_scala.go +++ b/pkg/resources/function_scala.go @@ -2,11 +2,17 @@ package resources import ( "context" + "errors" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +22,7 @@ func FunctionScala() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.FunctionScala, CreateContextFunctionScala), ReadContext: TrackingReadWrapper(resources.FunctionScala, ReadContextFunctionScala), - UpdateContext: TrackingUpdateWrapper(resources.FunctionScala, UpdateContextFunctionScala), + UpdateContext: TrackingUpdateWrapper(resources.FunctionScala, UpdateFunction("SCALA", ReadContextFunctionScala)), DeleteContext: TrackingDeleteWrapper(resources.FunctionScala, DeleteFunction), Description: "Resource used to manage scala function objects. For more information, check [function documentation](https://docs.snowflake.com/en/sql-reference/sql/create-function).", @@ -25,7 +31,11 @@ func FunctionScala() *schema.Resource { ComputedIfAnyAttributeChanged(scalaFunctionSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(functionParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllFunctionParameters), strings.ToLower)...), functionParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only potential option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("function_language", "SCALA"), )), Schema: collections.MergeMaps(scalaFunctionSchema, functionParametersSchema), @@ -36,17 +46,104 @@ func FunctionScala() *schema.Resource { } func CreateContextFunctionScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseFunctionArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returnTypeRaw := d.Get("return_type").(string) + returnDataType, err := datatypes.ParseDataType(returnTypeRaw) + if err != nil { + return diag.FromErr(err) + } + handler := d.Get("handler").(string) + runtimeVersion := d.Get("runtime_version").(string) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.FunctionArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForScalaFunctionRequest(id.SchemaObjectId(), returnDataType, runtimeVersion, handler). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + attributeMappedValueCreateBuilder[string](d, "return_results_behavior", request.WithReturnResultsBehavior, sdk.ToReturnResultsBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + setFunctionImportsInBuilder(d, request.WithImports), + setFunctionPackagesInBuilder(d, request.WithPackages), + setExternalAccessIntegrationsInBuilder(d, request.WithExternalAccessIntegrations), + setSecretsInBuilder(d, request.WithSecrets), + setFunctionTargetPathInBuilder(d, request.WithTargetPath), + stringAttributeCreateBuilder(d, "function_definition", request.WithFunctionDefinitionWrapped), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Functions.CreateForScala(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create function (query does not fail but parameters stay unchanged) + setRequest := sdk.NewFunctionSetRequest() + if parametersCreateDiags := handleFunctionParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewFunctionSetRequest()) { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextFunctionScala(ctx, d, meta) } func ReadContextFunctionScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } -func UpdateContextFunctionScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + allFunctionDetails, diags := queryAllFunctionDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allFunctionDetails.functionDetails.NormalizedArguments), + d.Set("return_type", allFunctionDetails.functionDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + // not reading return_results_behavior on purpose (handled as external change to show output) + setOptionalFromStringPtr(d, "runtime_version", allFunctionDetails.functionDetails.RuntimeVersion), + d.Set("comment", allFunctionDetails.function.Description), + readFunctionOrProcedureImports(d, allFunctionDetails.functionDetails.NormalizedImports), + d.Set("packages", allFunctionDetails.functionDetails.NormalizedPackages), + setRequiredFromStringPtr(d, "handler", allFunctionDetails.functionDetails.Handler), + readFunctionOrProcedureExternalAccessIntegrations(d, allFunctionDetails.functionDetails.NormalizedExternalAccessIntegrations), + readFunctionOrProcedureSecrets(d, allFunctionDetails.functionDetails.NormalizedSecrets), + readFunctionOrProcedureTargetPath(d, allFunctionDetails.functionDetails.NormalizedTargetPath), + setOptionalFromStringPtr(d, "function_definition", allFunctionDetails.functionDetails.Body), + d.Set("function_language", allFunctionDetails.functionDetails.Language), + + handleFunctionParameterRead(d, allFunctionDetails.functionParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.FunctionToSchema(allFunctionDetails.function)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.FunctionParametersToSchema(allFunctionDetails.functionParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func DeleteContextFunctionScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/function_sql.go b/pkg/resources/function_sql.go index cd8cb31dc8..53694da3a1 100644 --- a/pkg/resources/function_sql.go +++ b/pkg/resources/function_sql.go @@ -2,11 +2,17 @@ package resources import ( "context" + "errors" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +22,7 @@ func FunctionSql() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.FunctionSql, CreateContextFunctionSql), ReadContext: TrackingReadWrapper(resources.FunctionSql, ReadContextFunctionSql), - UpdateContext: TrackingUpdateWrapper(resources.FunctionSql, UpdateContextFunctionSql), + UpdateContext: TrackingUpdateWrapper(resources.FunctionSql, UpdateFunction("SQL", ReadContextFunctionSql)), DeleteContext: TrackingDeleteWrapper(resources.FunctionSql, DeleteFunction), Description: "Resource used to manage sql function objects. For more information, check [function documentation](https://docs.snowflake.com/en/sql-reference/sql/create-function).", @@ -25,7 +31,11 @@ func FunctionSql() *schema.Resource { ComputedIfAnyAttributeChanged(sqlFunctionSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(functionParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllFunctionParameters), strings.ToLower)...), functionParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only potential option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("function_language", "SQL"), )), Schema: collections.MergeMaps(sqlFunctionSchema, functionParametersSchema), @@ -36,17 +46,89 @@ func FunctionSql() *schema.Resource { } func CreateContextFunctionSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseFunctionArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returns, err := parseFunctionReturnsCommon(d) + if err != nil { + return diag.FromErr(err) + } + functionDefinition := d.Get("function_definition").(string) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.FunctionArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForSQLFunctionRequestDefinitionWrapped(id.SchemaObjectId(), *returns, functionDefinition). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "return_results_behavior", request.WithReturnResultsBehavior, sdk.ToReturnResultsBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Functions.CreateForSQL(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create function (query does not fail but parameters stay unchanged) + setRequest := sdk.NewFunctionSetRequest() + if parametersCreateDiags := handleFunctionParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewFunctionSetRequest()) { + err := client.Functions.Alter(ctx, sdk.NewAlterFunctionRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextFunctionSql(ctx, d, meta) } func ReadContextFunctionSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } -func UpdateContextFunctionSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + allFunctionDetails, diags := queryAllFunctionDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allFunctionDetails.functionDetails.NormalizedArguments), + d.Set("return_type", allFunctionDetails.functionDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + // not reading return_results_behavior on purpose (handled as external change to show output) + d.Set("comment", allFunctionDetails.function.Description), + setRequiredFromStringPtr(d, "handler", allFunctionDetails.functionDetails.Handler), + setOptionalFromStringPtr(d, "function_definition", allFunctionDetails.functionDetails.Body), + d.Set("function_language", allFunctionDetails.functionDetails.Language), + + handleFunctionParameterRead(d, allFunctionDetails.functionParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.FunctionToSchema(allFunctionDetails.function)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.FunctionParametersToSchema(allFunctionDetails.functionParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func DeleteContextFunctionSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/procedure_commons.go b/pkg/resources/procedure_commons.go index addb4f3c30..12d3645388 100644 --- a/pkg/resources/procedure_commons.go +++ b/pkg/resources/procedure_commons.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "log" + "reflect" "slices" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -401,6 +403,83 @@ func DeleteProcedure(ctx context.Context, d *schema.ResourceData, meta any) diag return nil } +func UpdateProcedure(language string, readFunc func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics) func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + newId := sdk.NewSchemaObjectIdentifierWithArgumentsInSchema(id.SchemaId(), d.Get("name").(string), id.ArgumentDataTypes()...) + + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithRenameTo(newId.SchemaObjectId())) + if err != nil { + return diag.FromErr(fmt.Errorf("error renaming procedure %v err = %w", d.Id(), err)) + } + + d.SetId(helpers.EncodeResourceIdentifier(newId)) + id = newId + } + + // Batch SET operations and UNSET operations + setRequest := sdk.NewProcedureSetRequest() + unsetRequest := sdk.NewProcedureUnsetRequest() + + _ = stringAttributeUpdate(d, "comment", &setRequest.Comment, &unsetRequest.Comment) + + switch language { + case "JAVA", "SCALA", "PYTHON": + err = errors.Join( + func() error { + if d.HasChange("secrets") { + return setSecretsInBuilder(d, func(references []sdk.SecretReference) *sdk.ProcedureSetRequest { + return setRequest.WithSecretsList(sdk.SecretsListRequest{SecretsList: references}) + }) + } + return nil + }(), + func() error { + if d.HasChange("external_access_integrations") { + return setExternalAccessIntegrationsInBuilder(d, func(references []sdk.AccountObjectIdentifier) any { + if len(references) == 0 { + return unsetRequest.WithExternalAccessIntegrations(true) + } else { + return setRequest.WithExternalAccessIntegrations(references) + } + }) + } + return nil + }(), + ) + if err != nil { + return diag.FromErr(err) + } + } + + if updateParamDiags := handleProcedureParametersUpdate(d, setRequest, unsetRequest); len(updateParamDiags) > 0 { + return updateParamDiags + } + + // Apply SET and UNSET changes + if !reflect.DeepEqual(*setRequest, *sdk.NewProcedureSetRequest()) { + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + if !reflect.DeepEqual(*unsetRequest, *sdk.NewProcedureUnsetRequest()) { + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithUnset(*unsetRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return readFunc(ctx, d, meta) + } +} + func queryAllProcedureDetailsCommon(ctx context.Context, d *schema.ResourceData, client *sdk.Client, id sdk.SchemaObjectIdentifierWithArguments) (*allProcedureDetailsCommon, diag.Diagnostics) { procedureDetails, err := client.Procedures.DescribeDetails(ctx, id) if err != nil { @@ -526,6 +605,26 @@ func parseProcedureReturnsCommon(d *schema.ResourceData) (*sdk.ProcedureReturnsR return returns, nil } +func parseProcedureSqlReturns(d *schema.ResourceData) (*sdk.ProcedureSQLReturnsRequest, error) { + returnTypeRaw := d.Get("return_type").(string) + dataType, err := datatypes.ParseDataType(returnTypeRaw) + if err != nil { + return nil, err + } + returns := sdk.NewProcedureSQLReturnsRequest() + switch v := dataType.(type) { + case *datatypes.TableDataType: + var cr []sdk.ProcedureColumnRequest + for _, c := range v.Columns() { + cr = append(cr, *sdk.NewProcedureColumnRequest(c.ColumnName(), c.ColumnType())) + } + returns.WithTable(*sdk.NewProcedureReturnsTableRequest().WithColumns(cr)) + default: + returns.WithResultDataType(*sdk.NewProcedureReturnsResultDataTypeRequest(dataType)) + } + return returns, nil +} + func setProcedureImportsInBuilder[T any](d *schema.ResourceData, setImports func([]sdk.ProcedureImportRequest) T) error { imports, err := parseProcedureImportsCommon(d) if err != nil { diff --git a/pkg/resources/procedure_java.go b/pkg/resources/procedure_java.go index 04fcb0cf1a..1d98f7cf2a 100644 --- a/pkg/resources/procedure_java.go +++ b/pkg/resources/procedure_java.go @@ -23,7 +23,7 @@ func ProcedureJava() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.ProcedureJava, CreateContextProcedureJava), ReadContext: TrackingReadWrapper(resources.ProcedureJava, ReadContextProcedureJava), - UpdateContext: TrackingUpdateWrapper(resources.ProcedureJava, UpdateContextProcedureJava), + UpdateContext: TrackingUpdateWrapper(resources.ProcedureJava, UpdateProcedure("JAVA", ReadContextProcedureJava)), DeleteContext: TrackingDeleteWrapper(resources.ProcedureJava, DeleteProcedure), Description: "Resource used to manage java procedure objects. For more information, check [procedure documentation](https://docs.snowflake.com/en/sql-reference/sql/create-procedure).", @@ -151,74 +151,3 @@ func ReadContextProcedureJava(ctx context.Context, d *schema.ResourceData, meta return nil } - -func UpdateContextProcedureJava(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := meta.(*provider.Context).Client - id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) - if err != nil { - return diag.FromErr(err) - } - - if d.HasChange("name") { - newId := sdk.NewSchemaObjectIdentifierWithArgumentsInSchema(id.SchemaId(), d.Get("name").(string), id.ArgumentDataTypes()...) - - err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithRenameTo(newId.SchemaObjectId())) - if err != nil { - return diag.FromErr(fmt.Errorf("error renaming procedure %v err = %w", d.Id(), err)) - } - - d.SetId(helpers.EncodeResourceIdentifier(newId)) - id = newId - } - - // Batch SET operations and UNSET operations - setRequest := sdk.NewProcedureSetRequest() - unsetRequest := sdk.NewProcedureUnsetRequest() - - err = errors.Join( - stringAttributeUpdate(d, "comment", &setRequest.Comment, &unsetRequest.Comment), - func() error { - if d.HasChange("secrets") { - return setSecretsInBuilder(d, func(references []sdk.SecretReference) *sdk.ProcedureSetRequest { - return setRequest.WithSecretsList(sdk.SecretsListRequest{SecretsList: references}) - }) - } - return nil - }(), - func() error { - if d.HasChange("external_access_integrations") { - return setExternalAccessIntegrationsInBuilder(d, func(references []sdk.AccountObjectIdentifier) any { - if len(references) == 0 { - return unsetRequest.WithExternalAccessIntegrations(true) - } else { - return setRequest.WithExternalAccessIntegrations(references) - } - }) - } - return nil - }(), - ) - if err != nil { - return diag.FromErr(err) - } - - if updateParamDiags := handleProcedureParametersUpdate(d, setRequest, unsetRequest); len(updateParamDiags) > 0 { - return updateParamDiags - } - - // Apply SET and UNSET changes - if !reflect.DeepEqual(*setRequest, *sdk.NewProcedureSetRequest()) { - err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithSet(*setRequest)) - if err != nil { - return diag.FromErr(err) - } - } - if !reflect.DeepEqual(*unsetRequest, *sdk.NewProcedureUnsetRequest()) { - err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithUnset(*unsetRequest)) - if err != nil { - return diag.FromErr(err) - } - } - - return ReadContextProcedureJava(ctx, d, meta) -} diff --git a/pkg/resources/procedure_javascript.go b/pkg/resources/procedure_javascript.go index 5088b492f7..4a273e28f5 100644 --- a/pkg/resources/procedure_javascript.go +++ b/pkg/resources/procedure_javascript.go @@ -2,11 +2,17 @@ package resources import ( "context" + "errors" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +22,7 @@ func ProcedureJavascript() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.ProcedureJavascript, CreateContextProcedureJavascript), ReadContext: TrackingReadWrapper(resources.ProcedureJavascript, ReadContextProcedureJavascript), - UpdateContext: TrackingUpdateWrapper(resources.ProcedureJavascript, UpdateContextProcedureJavascript), + UpdateContext: TrackingUpdateWrapper(resources.ProcedureJavascript, UpdateProcedure("JAVASCRIPT", ReadContextProcedureJavascript)), DeleteContext: TrackingDeleteWrapper(resources.ProcedureJavascript, DeleteProcedure), Description: "Resource used to manage javascript procedure objects. For more information, check [procedure documentation](https://docs.snowflake.com/en/sql-reference/sql/create-procedure).", @@ -25,7 +31,11 @@ func ProcedureJavascript() *schema.Resource { ComputedIfAnyAttributeChanged(javascriptProcedureSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(procedureParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllProcedureParameters), strings.ToLower)...), procedureParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("procedure_language", "JAVASCRIPT"), )), Schema: collections.MergeMaps(javascriptProcedureSchema, procedureParametersSchema), @@ -36,13 +46,88 @@ func ProcedureJavascript() *schema.Resource { } func CreateContextProcedureJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseProcedureArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returnTypeRaw := d.Get("return_type").(string) + returnDataType, err := datatypes.ParseDataType(returnTypeRaw) + if err != nil { + return diag.FromErr(err) + } + procedureDefinition := d.Get("procedure_definition").(string) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.ProcedureArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForJavaScriptProcedureRequestDefinitionWrapped(id.SchemaObjectId(), returnDataType, procedureDefinition). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Procedures.CreateForJavaScript(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create procedure (query does not fail but parameters stay unchanged) + setRequest := sdk.NewProcedureSetRequest() + if parametersCreateDiags := handleProcedureParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewProcedureSetRequest()) { + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextProcedureJavascript(ctx, d, meta) } func ReadContextProcedureJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + allProcedureDetails, diags := queryAllProcedureDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allProcedureDetails.procedureDetails.NormalizedArguments), + d.Set("return_type", allProcedureDetails.procedureDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + d.Set("comment", allProcedureDetails.procedure.Description), + setOptionalFromStringPtr(d, "procedure_definition", allProcedureDetails.procedureDetails.Body), + d.Set("procedure_language", allProcedureDetails.procedureDetails.Language), + + handleProcedureParameterRead(d, allProcedureDetails.procedureParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.ProcedureToSchema(allProcedureDetails.procedure)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.ProcedureParametersToSchema(allProcedureDetails.procedureParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func UpdateContextProcedureJavascript(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/procedure_python.go b/pkg/resources/procedure_python.go index 717cee32fe..0432fbb966 100644 --- a/pkg/resources/procedure_python.go +++ b/pkg/resources/procedure_python.go @@ -2,11 +2,18 @@ package resources import ( "context" + "errors" + "fmt" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +23,7 @@ func ProcedurePython() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.ProcedurePython, CreateContextProcedurePython), ReadContext: TrackingReadWrapper(resources.ProcedurePython, ReadContextProcedurePython), - UpdateContext: TrackingUpdateWrapper(resources.ProcedurePython, UpdateContextProcedurePython), + UpdateContext: TrackingUpdateWrapper(resources.ProcedurePython, UpdateProcedure("PYTHON", ReadContextProcedurePython)), DeleteContext: TrackingDeleteWrapper(resources.ProcedurePython, DeleteProcedure), Description: "Resource used to manage python procedure objects. For more information, check [procedure documentation](https://docs.snowflake.com/en/sql-reference/sql/create-procedure).", @@ -25,7 +32,11 @@ func ProcedurePython() *schema.Resource { ComputedIfAnyAttributeChanged(pythonProcedureSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(procedureParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllProcedureParameters), strings.ToLower)...), procedureParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("procedure_language", "PYTHON"), )), Schema: collections.MergeMaps(pythonProcedureSchema, procedureParametersSchema), @@ -36,13 +47,105 @@ func ProcedurePython() *schema.Resource { } func CreateContextProcedurePython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseProcedureArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returns, err := parseProcedureReturnsCommon(d) + if err != nil { + return diag.FromErr(err) + } + handler := d.Get("handler").(string) + runtimeVersion := d.Get("runtime_version").(string) + + packages, err := parseProceduresPackagesCommon(d) + if err != nil { + return diag.FromErr(err) + } + packages = append(packages, *sdk.NewProcedurePackageRequest(fmt.Sprintf(`%s%s`, sdk.PythonSnowparkPackageString, d.Get("snowpark_package").(string)))) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.ProcedureArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForPythonProcedureRequest(id.SchemaObjectId(), *returns, runtimeVersion, packages, handler). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + setProcedureImportsInBuilder(d, request.WithImports), + setExternalAccessIntegrationsInBuilder(d, request.WithExternalAccessIntegrations), + setSecretsInBuilder(d, request.WithSecrets), + stringAttributeCreateBuilder(d, "procedure_definition", request.WithProcedureDefinitionWrapped), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Procedures.CreateForPython(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create procedure (query does not fail but parameters stay unchanged) + setRequest := sdk.NewProcedureSetRequest() + if parametersCreateDiags := handleProcedureParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewProcedureSetRequest()) { + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextProcedurePython(ctx, d, meta) } func ReadContextProcedurePython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + allProcedureDetails, diags := queryAllProcedureDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allProcedureDetails.procedureDetails.NormalizedArguments), + d.Set("return_type", allProcedureDetails.procedureDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + setRequiredFromStringPtr(d, "runtime_version", allProcedureDetails.procedureDetails.RuntimeVersion), + d.Set("comment", allProcedureDetails.procedure.Description), + readFunctionOrProcedureImports(d, allProcedureDetails.procedureDetails.NormalizedImports), + d.Set("packages", allProcedureDetails.procedureDetails.NormalizedPackages), + d.Set("snowpark_package", allProcedureDetails.procedureDetails.SnowparkVersion), + setRequiredFromStringPtr(d, "handler", allProcedureDetails.procedureDetails.Handler), + readFunctionOrProcedureExternalAccessIntegrations(d, allProcedureDetails.procedureDetails.NormalizedExternalAccessIntegrations), + readFunctionOrProcedureSecrets(d, allProcedureDetails.procedureDetails.NormalizedSecrets), + setOptionalFromStringPtr(d, "procedure_definition", allProcedureDetails.procedureDetails.Body), + d.Set("procedure_language", allProcedureDetails.procedureDetails.Language), + + handleProcedureParameterRead(d, allProcedureDetails.procedureParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.ProcedureToSchema(allProcedureDetails.procedure)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.ProcedureParametersToSchema(allProcedureDetails.procedureParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func UpdateContextProcedurePython(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/procedure_scala.go b/pkg/resources/procedure_scala.go index 793663d0e1..0a5dc691d0 100644 --- a/pkg/resources/procedure_scala.go +++ b/pkg/resources/procedure_scala.go @@ -2,11 +2,18 @@ package resources import ( "context" + "errors" + "fmt" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +23,7 @@ func ProcedureScala() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.ProcedureScala, CreateContextProcedureScala), ReadContext: TrackingReadWrapper(resources.ProcedureScala, ReadContextProcedureScala), - UpdateContext: TrackingUpdateWrapper(resources.ProcedureScala, UpdateContextProcedureScala), + UpdateContext: TrackingUpdateWrapper(resources.ProcedureScala, UpdateProcedure("SQL", ReadContextProcedureScala)), DeleteContext: TrackingDeleteWrapper(resources.ProcedureScala, DeleteProcedure), Description: "Resource used to manage scala procedure objects. For more information, check [procedure documentation](https://docs.snowflake.com/en/sql-reference/sql/create-procedure).", @@ -25,7 +32,11 @@ func ProcedureScala() *schema.Resource { ComputedIfAnyAttributeChanged(scalaProcedureSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(procedureParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllProcedureParameters), strings.ToLower)...), procedureParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("procedure_language", "SCALA"), )), Schema: collections.MergeMaps(scalaProcedureSchema, procedureParametersSchema), @@ -36,13 +47,107 @@ func ProcedureScala() *schema.Resource { } func CreateContextProcedureScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseProcedureArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returns, err := parseProcedureReturnsCommon(d) + if err != nil { + return diag.FromErr(err) + } + handler := d.Get("handler").(string) + runtimeVersion := d.Get("runtime_version").(string) + + packages, err := parseProceduresPackagesCommon(d) + if err != nil { + return diag.FromErr(err) + } + packages = append(packages, *sdk.NewProcedurePackageRequest(fmt.Sprintf(`%s%s`, sdk.JavaSnowparkPackageString, d.Get("snowpark_package").(string)))) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.ProcedureArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForScalaProcedureRequest(id.SchemaObjectId(), *returns, runtimeVersion, packages, handler). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + setProcedureImportsInBuilder(d, request.WithImports), + setExternalAccessIntegrationsInBuilder(d, request.WithExternalAccessIntegrations), + setSecretsInBuilder(d, request.WithSecrets), + setProcedureTargetPathInBuilder(d, request.WithTargetPath), + stringAttributeCreateBuilder(d, "procedure_definition", request.WithProcedureDefinitionWrapped), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Procedures.CreateForScala(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create procedure (query does not fail but parameters stay unchanged) + setRequest := sdk.NewProcedureSetRequest() + if parametersCreateDiags := handleProcedureParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewProcedureSetRequest()) { + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextProcedureScala(ctx, d, meta) } func ReadContextProcedureScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + allProcedureDetails, diags := queryAllProcedureDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allProcedureDetails.procedureDetails.NormalizedArguments), + d.Set("return_type", allProcedureDetails.procedureDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + setRequiredFromStringPtr(d, "runtime_version", allProcedureDetails.procedureDetails.RuntimeVersion), + d.Set("comment", allProcedureDetails.procedure.Description), + readFunctionOrProcedureImports(d, allProcedureDetails.procedureDetails.NormalizedImports), + d.Set("packages", allProcedureDetails.procedureDetails.NormalizedPackages), + d.Set("snowpark_package", allProcedureDetails.procedureDetails.SnowparkVersion), + setRequiredFromStringPtr(d, "handler", allProcedureDetails.procedureDetails.Handler), + readFunctionOrProcedureExternalAccessIntegrations(d, allProcedureDetails.procedureDetails.NormalizedExternalAccessIntegrations), + readFunctionOrProcedureSecrets(d, allProcedureDetails.procedureDetails.NormalizedSecrets), + readFunctionOrProcedureTargetPath(d, allProcedureDetails.procedureDetails.NormalizedTargetPath), + setOptionalFromStringPtr(d, "procedure_definition", allProcedureDetails.procedureDetails.Body), + d.Set("procedure_language", allProcedureDetails.procedureDetails.Language), + + handleProcedureParameterRead(d, allProcedureDetails.procedureParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.ProcedureToSchema(allProcedureDetails.procedure)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.ProcedureParametersToSchema(allProcedureDetails.procedureParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func UpdateContextProcedureScala(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil } diff --git a/pkg/resources/procedure_sql.go b/pkg/resources/procedure_sql.go index 11fcd69413..64ddfde270 100644 --- a/pkg/resources/procedure_sql.go +++ b/pkg/resources/procedure_sql.go @@ -2,11 +2,17 @@ package resources import ( "context" + "errors" + "reflect" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/datatypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -16,7 +22,7 @@ func ProcedureSql() *schema.Resource { return &schema.Resource{ CreateContext: TrackingCreateWrapper(resources.ProcedureSql, CreateContextProcedureSql), ReadContext: TrackingReadWrapper(resources.ProcedureSql, ReadContextProcedureSql), - UpdateContext: TrackingUpdateWrapper(resources.ProcedureSql, UpdateContextProcedureSql), + UpdateContext: TrackingUpdateWrapper(resources.ProcedureSql, UpdateProcedure("SQL", ReadContextProcedureSql)), DeleteContext: TrackingDeleteWrapper(resources.ProcedureSql, DeleteProcedure), Description: "Resource used to manage sql procedure objects. For more information, check [procedure documentation](https://docs.snowflake.com/en/sql-reference/sql/create-procedure).", @@ -25,7 +31,11 @@ func ProcedureSql() *schema.Resource { ComputedIfAnyAttributeChanged(sqlProcedureSchema, FullyQualifiedNameAttributeName, "name"), ComputedIfAnyAttributeChanged(procedureParametersSchema, ParametersAttributeName, collections.Map(sdk.AsStringList(sdk.AllProcedureParameters), strings.ToLower)...), procedureParametersCustomDiff, - // TODO[SNOW-1348103]: recreate when type changed externally + // The language check is more for the future. + // Currently, almost all attributes are marked as forceNew. + // When language changes, these attributes also change, causing the object to recreate either way. + // The only option is java staged <-> scala staged (however scala need runtime_version which may interfere). + RecreateWhenResourceStringFieldChangedExternally("procedure_language", "SQL"), )), Schema: collections.MergeMaps(sqlProcedureSchema, procedureParametersSchema), @@ -36,13 +46,87 @@ func ProcedureSql() *schema.Resource { } func CreateContextProcedureSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil + client := meta.(*provider.Context).Client + database := d.Get("database").(string) + sc := d.Get("schema").(string) + name := d.Get("name").(string) + + argumentRequests, err := parseProcedureArgumentsCommon(d) + if err != nil { + return diag.FromErr(err) + } + returns, err := parseProcedureSqlReturns(d) + if err != nil { + return diag.FromErr(err) + } + procedureDefinition := d.Get("procedure_definition").(string) + + argumentDataTypes := collections.Map(argumentRequests, func(r sdk.ProcedureArgumentRequest) datatypes.DataType { return r.ArgDataType }) + id := sdk.NewSchemaObjectIdentifierWithArgumentsNormalized(database, sc, name, argumentDataTypes...) + request := sdk.NewCreateForSQLProcedureRequestDefinitionWrapped(id.SchemaObjectId(), *returns, procedureDefinition). + WithArguments(argumentRequests) + + errs := errors.Join( + booleanStringAttributeCreateBuilder(d, "is_secure", request.WithSecure), + attributeMappedValueCreateBuilder[string](d, "null_input_behavior", request.WithNullInputBehavior, sdk.ToNullInputBehavior), + stringAttributeCreateBuilder(d, "comment", request.WithComment), + ) + if errs != nil { + return diag.FromErr(errs) + } + + if err := client.Procedures.CreateForSQL(ctx, request); err != nil { + return diag.FromErr(err) + } + d.SetId(helpers.EncodeResourceIdentifier(id)) + + // parameters do not work in create procedure (query does not fail but parameters stay unchanged) + setRequest := sdk.NewProcedureSetRequest() + if parametersCreateDiags := handleProcedureParametersCreate(d, setRequest); len(parametersCreateDiags) > 0 { + return parametersCreateDiags + } + if !reflect.DeepEqual(*setRequest, *sdk.NewProcedureSetRequest()) { + err := client.Procedures.Alter(ctx, sdk.NewAlterProcedureRequest(id).WithSet(*setRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextProcedureSql(ctx, d, meta) } func ReadContextProcedureSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return nil -} + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifierWithArguments(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + allProcedureDetails, diags := queryAllProcedureDetailsCommon(ctx, d, client, id) + if diags != nil { + return diags + } + + // TODO [SNOW-1348103]: handle external changes marking + // TODO [SNOW-1348103]: handle setting state to value from config + + errs := errors.Join( + // not reading is_secure on purpose (handled as external change to show output) + readFunctionOrProcedureArguments(d, allProcedureDetails.procedureDetails.NormalizedArguments), + d.Set("return_type", allProcedureDetails.procedureDetails.ReturnDataType.ToSql()), + // not reading null_input_behavior on purpose (handled as external change to show output) + d.Set("comment", allProcedureDetails.procedure.Description), + setOptionalFromStringPtr(d, "procedure_definition", allProcedureDetails.procedureDetails.Body), + d.Set("procedure_language", allProcedureDetails.procedureDetails.Language), + + handleProcedureParameterRead(d, allProcedureDetails.procedureParameters), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.ProcedureToSchema(allProcedureDetails.procedure)}), + d.Set(ParametersAttributeName, []map[string]any{schemas.ProcedureParametersToSchema(allProcedureDetails.procedureParameters)}), + ) + if errs != nil { + return diag.FromErr(err) + } -func UpdateContextProcedureSql(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { return nil }