diff --git a/pkg/resources/function_commons.go b/pkg/resources/function_commons.go index c116a89e3a..12ea55bd73 100644 --- a/pkg/resources/function_commons.go +++ b/pkg/resources/function_commons.go @@ -88,7 +88,6 @@ var ( "is_secure", "arguments", "return_type", - "null_input_behavior", "return_results_behavior", "comment", "function_definition", @@ -100,6 +99,7 @@ var ( javaFunctionSchemaDefinition = functionSchemaDef{ additionalArguments: []string{ "runtime_version", + "null_input_behavior", "imports", "packages", "handler", @@ -117,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, } @@ -125,6 +127,7 @@ var ( additionalArguments: []string{ "is_aggregate", "runtime_version", + "null_input_behavior", "imports", "packages", "handler", @@ -141,6 +144,7 @@ var ( scalaFunctionSchemaDefinition = functionSchemaDef{ additionalArguments: []string{ "runtime_version", + "null_input_behavior", "imports", "packages", "handler", diff --git a/pkg/resources/function_javascript.go b/pkg/resources/function_javascript.go index 9da0464ef6..fe0884dd67 100644 --- a/pkg/resources/function_javascript.go +++ b/pkg/resources/function_javascript.go @@ -59,11 +59,11 @@ func CreateContextFunctionJavascript(ctx context.Context, d *schema.ResourceData if err != nil { return diag.FromErr(err) } - handler := d.Get("handler").(string) + 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, handler). + request := sdk.NewCreateForJavascriptFunctionRequestDefinitionWrapped(id.SchemaObjectId(), *returns, functionDefinition). WithArguments(argumentRequests) errs := errors.Join( diff --git a/pkg/resources/function_sql.go b/pkg/resources/function_sql.go index 07dd3c38f9..53694da3a1 100644 --- a/pkg/resources/function_sql.go +++ b/pkg/resources/function_sql.go @@ -59,11 +59,11 @@ func CreateContextFunctionSql(ctx context.Context, d *schema.ResourceData, meta if err != nil { return diag.FromErr(err) } - handler := d.Get("handler").(string) + 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, handler). + request := sdk.NewCreateForSQLFunctionRequestDefinitionWrapped(id.SchemaObjectId(), *returns, functionDefinition). WithArguments(argumentRequests) errs := errors.Join( 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 }