Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use external functions #2454

Merged
merged 6 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Migration guide

This document is meant to help you migrate your Terraform config to the new newest version. In migration guides, we will only
This document is meant to help you migrate your Terraform config to the new newest version. In migration guides, we will only
describe deprecations or breaking changes and help you to change your configuration to keep the same (or similar) behavior
across different versions.

Expand All @@ -14,6 +14,19 @@ It is noted as a behavior change but in some way it is not; with the previous im

We will consider adding `NOT NULL` back because it can be set by `ALTER COLUMN columnX SET NOT NULL`, but first we want to revisit the whole resource design.

### snowflake_external_function resource changes

#### *(behavior change)* return_null_allowed default is now true
The `return_null_allowed` attribute default value is now `true`. This is a behavior change because it was `false` before. The reason it was changed is to match the expected default value in the [documentation](https://docs.snowflake.com/en/sql-reference/sql/create-external-function#optional-parameters) `Default: The default is NULL (i.e. the function can return NULL values).`

#### *(behavior change)* comment is no longer required
sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved
The `comment` attribute is now optional. It was required before, but it is not required in Snowflake API.

### snowflake_external_functions data source changes

#### *(behavior change)* schema is now required with database
The `schema` attribute is now required with `database` attribute to match old implementation `SHOW EXTERNAL FUNCTIONS IN SCHEMA "<database>"."<schema>"`. In the future this may change to make schema optional.

## vX.XX.X -> v0.85.0

### Migration from old (grant) resources to new ones
Expand Down Expand Up @@ -47,7 +60,7 @@ Force new was added for the following attributes (because no usable SQL alter st
## v0.73.0 ➞ v0.74.0
### Provider configuration changes

In this change we have done a provider refactor to make it more complete and customizable by supporting more options that
In this change we have done a provider refactor to make it more complete and customizable by supporting more options that
were already available in Golang Snowflake driver. This lead to several attributes being added and a few deprecated.
We will focus on the deprecated ones and show you how to adapt your current configuration to the new changes.

Expand All @@ -57,7 +70,7 @@ We will focus on the deprecated ones and show you how to adapt your current conf
provider "snowflake" {
# before
username = "username"

# after
user = "username"
}
Expand Down Expand Up @@ -123,7 +136,7 @@ provider "snowflake" {
provider "snowflake" {
# before
session_params = {}

# after
params = {}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/data-sources/external_functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ data "snowflake_external_functions" "current" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `database` (String) The database from which to return the schemas from.
- `schema` (String) The schema from which to return the external functions from.
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/external_function.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ resource "snowflake_external_function" "test_ext_func" {
- `null_input_behavior` (String) Specifies the behavior of the external function when called with null inputs.
- `request_translator` (String) This specifies the name of the request translator function
- `response_translator` (String) This specifies the name of the response translator function.
- `return_null_allowed` (Boolean) Indicates whether the function can return NULL values or must return only NON-NULL values.
- `return_null_allowed` (Boolean) Indicates whether the function can return NULL values (true) or must return only NON-NULL values (false).

### Read-Only

Expand Down
75 changes: 47 additions & 28 deletions pkg/datasources/external_functions.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
package datasources

import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"strings"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var externalFunctionsSchema = map[string]*schema.Schema{
"database": {
Type: schema.TypeString,
Required: true,
Optional: true,
Description: "The database from which to return the schemas from.",
},
"schema": {
Type: schema.TypeString,
Required: true,
Description: "The schema from which to return the external functions from.",
Type: schema.TypeString,
Optional: true,
RequiredWith: []string{"database"},
Description: "The schema from which to return the external functions from.",
},
"external_functions": {
Type: schema.TypeList,
Expand Down Expand Up @@ -56,42 +58,59 @@ var externalFunctionsSchema = map[string]*schema.Schema{

func ExternalFunctions() *schema.Resource {
return &schema.Resource{
Read: ReadExternalFunctions,
Schema: externalFunctionsSchema,
ReadContext: ReadContextExternalFunctions,
Schema: externalFunctionsSchema,
}
}

func ReadExternalFunctions(d *schema.ResourceData, meta interface{}) error {
func ReadContextExternalFunctions(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
db := meta.(*sql.DB)
client := sdk.NewClientFromDB(db)
databaseName := d.Get("database").(string)
schemaName := d.Get("schema").(string)

currentExternalFunctions, err := snowflake.ListExternalFunctions(databaseName, schemaName, db)
if errors.Is(err, sql.ErrNoRows) {
// If not found, mark resource to be removed from state file during apply or refresh
log.Printf("[DEBUG] external functions in schema (%s) not found", d.Id())
d.SetId("")
return nil
} else if err != nil {
log.Printf("[DEBUG] unable to parse external functions in schema (%s)", d.Id())
req := sdk.NewShowExternalFunctionRequest()
externalFunctions, err := client.ExternalFunctions.Show(ctx, req)
sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
d.SetId("")
return nil
}

externalFunctions := []map[string]interface{}{}

for _, externalFunction := range currentExternalFunctions {
externalFunctionsList := []map[string]interface{}{}
for _, externalFunction := range externalFunctions {
externalFunctionMap := map[string]interface{}{}
externalFunctionMap["name"] = externalFunction.Name

// do we filter by database?
currentDatabase := strings.Trim(externalFunction.CatalogName, `"`)
if databaseName != "" {
if currentDatabase != databaseName {
continue
}
externalFunctionMap["database"] = currentDatabase
} else {
externalFunctionMap["database"] = currentDatabase
}

externalFunctionMap["name"] = externalFunction.ExternalFunctionName.String
externalFunctionMap["database"] = externalFunction.DatabaseName.String
externalFunctionMap["schema"] = externalFunction.SchemaName.String
externalFunctionMap["comment"] = externalFunction.Comment.String
externalFunctionMap["language"] = externalFunction.Language.String
// do we filter by schema?
currentSchema := strings.Trim(externalFunction.SchemaName, `"`)
if schemaName != "" {
if currentSchema != schemaName {
continue
}
externalFunctionMap["schema"] = currentSchema
} else {
externalFunctionMap["schema"] = currentSchema
}

externalFunctions = append(externalFunctions, externalFunctionMap)
externalFunctionMap["comment"] = externalFunction.Description
externalFunctionMap["language"] = externalFunction.Language
externalFunctionsList = append(externalFunctionsList, externalFunctionMap)
}

d.SetId(fmt.Sprintf(`%v|%v`, databaseName, schemaName))
return d.Set("external_functions", externalFunctions)
if err := d.Set("external_functions", externalFunctionsList); err != nil {
return diag.FromErr(err)
}
return nil
}
130 changes: 75 additions & 55 deletions pkg/datasources/external_functions_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -1,80 +1,100 @@
package datasources_test

import (
"fmt"
"os"
"strings"
"testing"

acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance"

"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestAcc_ExternalFunctions(t *testing.T) {
databaseName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
schemaName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
apiName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
externalFunctionName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
func TestAcc_ExternalFunctions_basic(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_EXTERNAL_FUNCTION_TESTS"); ok {
t.Skip("Skipping TestAcc_ExternalFunction")
}
accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
m := func() map[string]config.Variable {
return map[string]config.Variable{
"database": config.StringVariable(acc.TestDatabaseName),
sfc-gh-asawicki marked this conversation as resolved.
Show resolved Hide resolved
"schema": config.StringVariable(acc.TestSchemaName),
"name": config.StringVariable(accName),
"api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")),
"url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"),
"comment": config.StringVariable("Terraform acceptance test"),
}
}

dataSourceName := "data.snowflake_external_functions.external_functions"
configVariables := m()

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: externalFunctions(databaseName, schemaName, apiName, externalFunctionName),
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunctions/basic"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "database", databaseName),
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "schema", schemaName),
resource.TestCheckResourceAttrSet("data.snowflake_external_functions.t", "external_functions.#"),
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "external_functions.#", "1"),
resource.TestCheckResourceAttr("data.snowflake_external_functions.t", "external_functions.0.name", externalFunctionName),
resource.TestCheckResourceAttr(dataSourceName, "database", acc.TestDatabaseName),
resource.TestCheckResourceAttr(dataSourceName, "schema", acc.TestSchemaName),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.#"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.name"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.database"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.schema"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.comment"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.language"),
),
},
},
})
}

func externalFunctions(databaseName string, schemaName string, apiName string, externalFunctionName string) string {
return fmt.Sprintf(`

resource snowflake_database "test_database" {
name = "%v"
}

resource snowflake_schema "test_schema"{
name = "%v"
database = snowflake_database.test_database.name
func TestAcc_ExternalFunctions_no_database(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_EXTERNAL_FUNCTION_TESTS"); ok {
t.Skip("Skipping TestAcc_ExternalFunction")
}

resource "snowflake_api_integration" "test_api_int" {
name = "%v"
api_provider = "aws_api_gateway"
api_aws_role_arn = "arn:aws:iam::000000000001:/role/test"
api_allowed_prefixes = ["https://123456.execute-api.us-west-2.amazonaws.com/prod/"]
enabled = true
}

resource "snowflake_external_function" "test_func" {
name = "%v"
database = snowflake_database.test_database.name
schema = snowflake_schema.test_schema.name
arg {
name = "arg1"
type = "varchar"
accName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
m := func() map[string]config.Variable {
return map[string]config.Variable{
"database": config.StringVariable(acc.TestDatabaseName),
"schema": config.StringVariable(acc.TestSchemaName),
"name": config.StringVariable(accName),
"api_allowed_prefixes": config.ListVariable(config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/")),
"url_of_proxy_and_resource": config.StringVariable("https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"),
"comment": config.StringVariable("Terraform acceptance test"),
}
arg {
name = "arg2"
type = "varchar"
}
comment = "Terraform acceptance test"
return_type = "varchar"
return_behavior = "IMMUTABLE"
api_integration = snowflake_api_integration.test_api_int.name
url_of_proxy_and_resource = "https://123456.execute-api.us-west-2.amazonaws.com/prod/test_func"
}

data snowflake_external_functions "t" {
database = snowflake_external_function.test_func.database
schema = snowflake_external_function.test_func.schema
depends_on = [snowflake_external_function.test_func]
}
`, databaseName, schemaName, apiName, externalFunctionName)
dataSourceName := "data.snowflake_external_functions.external_functions"
configVariables := m()

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: nil,
Steps: []resource.TestStep{
{
ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ExternalFunctions/no_filter"),
ConfigVariables: configVariables,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.#"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.name"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.comment"),
resource.TestCheckResourceAttrSet(dataSourceName, "external_functions.0.language"),
),
},
},
})
}
31 changes: 31 additions & 0 deletions pkg/datasources/testdata/TestAcc_ExternalFunctions/basic/test.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
resource "snowflake_api_integration" "test_api_int" {
name = var.name
api_provider = "aws_api_gateway"
api_aws_role_arn = "arn:aws:iam::000000000001:/role/test"
api_allowed_prefixes = var.api_allowed_prefixes
enabled = true
}

resource "snowflake_external_function" "external_function" {
name = var.name
database = var.database
schema = var.schema
arg {
name = "ARG1"
type = "VARCHAR"
}
arg {
name = "ARG2"
type = "VARCHAR"
}
comment = var.comment
return_type = "VARIANT"
return_behavior = "IMMUTABLE"
api_integration = snowflake_api_integration.test_api_int.name
url_of_proxy_and_resource = var.url_of_proxy_and_resource
}

data "snowflake_external_functions" "external_functions" {
database = snowflake_external_function.external_function.database
schema = snowflake_external_function.external_function.schema
}
Loading
Loading