Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-jcieslak committed Aug 5, 2024
1 parent 8328e32 commit 5767204
Show file tree
Hide file tree
Showing 10 changed files with 1,558 additions and 1,398 deletions.
991 changes: 487 additions & 504 deletions pkg/resources/external_function.go

Large diffs are not rendered by default.

1,106 changes: 546 additions & 560 deletions pkg/resources/external_function_acceptance_test.go

Large diffs are not rendered by default.

80 changes: 78 additions & 2 deletions pkg/resources/function_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package resources_test

import (
"fmt"
"regexp"
"strings"
"testing"

Expand Down Expand Up @@ -218,8 +219,13 @@ func TestAcc_Function_migrateFromVersion085(t *testing.T) {
),
},
{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
Config: functionConfig(acc.TestDatabaseName, acc.TestSchemaName, name, comment),
ExternalProviders: map[string]resource.ExternalProvider{
"snowflake": {
VersionConstraint: "=0.94.1",
Source: "Snowflake-Labs/snowflake",
},
},
Config: functionConfig(acc.TestDatabaseName, acc.TestSchemaName, name, comment),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "id", id.FullyQualifiedName()),
resource.TestCheckResourceAttr(resourceName, "name", name),
Expand All @@ -231,6 +237,41 @@ func TestAcc_Function_migrateFromVersion085(t *testing.T) {
})
}

func TestAcc_Function_Version0941_ResourceIdMigration(t *testing.T) {
name := acc.TestClient().Ids.RandomAccountObjectIdentifier().Name()
resourceName := "snowflake_function.f"

resource.Test(t, resource.TestCase{
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: acc.CheckDestroy(t, resources.Function),
Steps: []resource.TestStep{
{
ExternalProviders: map[string]resource.ExternalProvider{
"snowflake": {
VersionConstraint: "=0.94.1",
Source: "Snowflake-Labs/snowflake",
},
},
Config: functionConfigWithVector(acc.TestDatabaseName, acc.TestSchemaName, name, ""),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%s"."%s"."%s"(VARCHAR, VECTOR(INT, 20), FLOAT, NUMBER, VECTOR(FLOAT, 10))`, acc.TestDatabaseName, acc.TestSchemaName, name)),
),
ExpectError: regexp.MustCompile("Error: invalid data type: VECTOR\\(INT, 20\\)"),
},
{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
Config: functionConfigWithVector(acc.TestDatabaseName, acc.TestSchemaName, name, ""),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf(`"%s"."%s"."%s"(VARCHAR, VECTOR(INT, 20), FLOAT, NUMBER, VECTOR(FLOAT, 10))`, acc.TestDatabaseName, acc.TestSchemaName, name)),
),
},
},
})
}

func TestAcc_Function_Rename(t *testing.T) {
name := acc.TestClient().Ids.Alpha()
newName := acc.TestClient().Ids.Alpha()
Expand Down Expand Up @@ -272,6 +313,41 @@ func TestAcc_Function_Rename(t *testing.T) {
})
}

func functionConfigWithVector(database string, schema string, name string, comment string) string {
return fmt.Sprintf(`
resource "snowflake_function" "f" {
database = "%[1]s"
schema = "%[2]s"
name = "%[3]s"
comment = "%[4]s"
return_type = "VARCHAR"
return_behavior = "IMMUTABLE"
statement = "SELECT A"
arguments {
name = "A"
type = "VARCHAR(200)"
}
arguments {
name = "B"
type = "VECTOR(INT, 20)"
}
arguments {
name = "C"
type = "FLOAT"
}
arguments {
name = "D"
type = "NUMBER(10, 2)"
}
arguments {
name = "E"
type = "VECTOR(FLOAT, 10)"
}
}
`, database, schema, name, comment)
}

func functionConfig(database string, schema string, name string, comment string) string {
return fmt.Sprintf(`
resource "snowflake_function" "f" {
Expand Down
1 change: 1 addition & 0 deletions pkg/resources/function_state_upgraders.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func parseV085FunctionId(v string) (*v085FunctionId, error) {
ArgTypes: args,
}, nil
}

func v085FunctionIdStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
if rawState == nil {
return rawState, nil
Expand Down
71 changes: 11 additions & 60 deletions pkg/sdk/functions_gen.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package sdk

import (
"bytes"
"context"
"database/sql"
"fmt"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections"
"log"
"strings"
)

Expand Down Expand Up @@ -226,15 +223,15 @@ type functionRow struct {
}

type Function struct {
CreatedOn string
Name string
SchemaName string
IsBuiltin bool
IsAggregate bool
IsAnsi bool
MinNumArguments int
MaxNumArguments int
// TODO(SNOW-function refactor): Remove raw arguments
CreatedOn string
Name string
SchemaName string
IsBuiltin bool
IsAggregate bool
IsAnsi bool
MinNumArguments int
MaxNumArguments int
Arguments []DataType
ArgumentsRaw string
Description string
CatalogName string
Expand Down Expand Up @@ -264,54 +261,8 @@ func parseFunctionArgumentsFromDetails(details []FunctionDetail) ([]DataType, er
return arguments, nil
}

// Move to sdk/identifier_parsers.go
func parseFunctionArgumentsFromString(arguments string) ([]DataType, error) {
dataTypes := make([]DataType, 0)

stringBuffer := bytes.NewBufferString(arguments)
for stringBuffer.Len() > 0 {

// we use another buffer to peek into next data type
peekBuffer := bytes.NewBufferString(stringBuffer.String())
peekDataType, _ := peekBuffer.ReadString(',')
peekDataType = strings.TrimSpace(peekDataType)

// For function purposes only Vector needs special case
switch {
case strings.HasPrefix(peekDataType, "VECTOR"):
vectorDataType, _ := stringBuffer.ReadString(')')
vectorDataType = strings.TrimSpace(vectorDataType)
if stringBuffer.Len() > 0 {
commaByte, err := stringBuffer.ReadByte()
if commaByte != ',' {
return nil, fmt.Errorf("expected a comma delimited string but found %s", string(commaByte))
}
if err != nil {
return nil, err
}
}
log.Println("Adding vec:", vectorDataType)
dataTypes = append(dataTypes, DataType(vectorDataType))
default:
dataType, err := stringBuffer.ReadString(',')
if err == nil {
dataType = dataType[:len(dataType)-1]
}
dataType = strings.TrimSpace(dataType)
log.Println("Adding:", dataType)
dataTypes = append(dataTypes, DataType(dataType))
}
}

return dataTypes, nil
}

func (v *Function) ID(details []FunctionDetail) (SchemaObjectIdentifierWithArguments, error) {
arguments, err := parseFunctionArgumentsFromDetails(details)
if err != nil {
return SchemaObjectIdentifierWithArguments{}, err
}
return NewSchemaObjectIdentifierWithArguments(v.CatalogName, v.SchemaName, v.Name, arguments...), nil
func (v *Function) ID() SchemaObjectIdentifierWithArguments {
return NewSchemaObjectIdentifierWithArguments(v.CatalogName, v.SchemaName, v.Name, v.Arguments...)
}

// DescribeFunctionOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-function.
Expand Down
11 changes: 11 additions & 0 deletions pkg/sdk/functions_impl_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package sdk
import (
"context"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections"
"log"
"strings"
)

var _ Functions = (*functions)(nil)
Expand Down Expand Up @@ -379,6 +381,15 @@ func (r functionRow) convert() *Function {
IsExternalFunction: r.IsExternalFunction == "Y",
Language: r.Language,
}
arguments := strings.TrimLeft(r.Arguments, r.Name)
returnIndex := strings.Index(arguments, ") RETURN ")
dataTypes, err := ParseFunctionArgumentsFromString(arguments[:returnIndex+1])
if err != nil {
log.Printf("[DEBUG] failed to parse function arguments, err = %s", err)
} else {
e.Arguments = dataTypes
}

if r.IsSecure.Valid {
e.IsSecure = r.IsSecure.String == "Y"
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/sdk/identifier_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,7 @@ func NewSchemaObjectIdentifierWithArgumentsFromFullyQualifiedName(fullyQualified
if err != nil {
return SchemaObjectIdentifierWithArguments{}, err
}
arguments := fullyQualifiedName[splitIdIndex:]
dataTypes, err := parseFunctionArgumentsFromString(arguments[1 : len(arguments)-1])
dataTypes, err := ParseFunctionArgumentsFromString(fullyQualifiedName[splitIdIndex:])
if err != nil {
return SchemaObjectIdentifierWithArguments{}, err
}
Expand Down
52 changes: 52 additions & 0 deletions pkg/sdk/identifier_parsers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sdk

import (
"bytes"
"encoding/csv"
"fmt"
"strings"
Expand Down Expand Up @@ -37,6 +38,10 @@ func ParseIdentifierString(identifier string) ([]string, error) {
if strings.Contains(part, `"`) {
return nil, fmt.Errorf(`unable to parse identifier: %s, currently identifiers containing double quotes are not supported in the provider`, identifier)
}
// TODO(SNOW-1571674): Remove the validation
if strings.ContainsAny(part, `()`) {
return nil, fmt.Errorf(`unable to parse identifier: %s, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`, identifier)
}
}
return parts, nil
}
Expand Down Expand Up @@ -140,3 +145,50 @@ func ParseExternalObjectIdentifier(identifier string) (ExternalObjectIdentifier,
},
)
}

// ParseFunctionArgumentsFromString parses function argument from arguments string with optional argument names.
// Varying types are not supported (e.g. VARCHAR(200)), because Snowflake outputs them in shortened version
// (VARCHAR in this case). The only exception is newly added type VECTOR which has the following structure
// VECTOR(<type>, n) where <type> right now can be either INT or FLOAT and n is the number of elements in the VECTOR.
// Snowflake returns vectors with their exact type and this function supports it.
func ParseFunctionArgumentsFromString(arguments string) ([]DataType, error) {
dataTypes := make([]DataType, 0)

if len(arguments) > 0 && arguments[0] == '(' && arguments[len(arguments)-1] == ')' {
arguments = arguments[1 : len(arguments)-1]
}
stringBuffer := bytes.NewBufferString(arguments)

for stringBuffer.Len() > 0 {
// We use another buffer to peek into next data type (needed for vector parsing)
peekDataType, _ := bytes.NewBufferString(stringBuffer.String()).ReadString(',')
peekDataType = strings.TrimSpace(peekDataType)

switch {
// For now, only vectors need special parsing behavior
case strings.HasPrefix(peekDataType, "VECTOR"):
vectorDataType, _ := stringBuffer.ReadString(')')
vectorDataType = strings.TrimSpace(vectorDataType)

if stringBuffer.Len() > 0 {
commaByte, err := stringBuffer.ReadByte()
if commaByte != ',' {
return nil, fmt.Errorf("expected a comma delimited string but found %s", string(commaByte))
}
if err != nil {
return nil, err
}
}
dataTypes = append(dataTypes, DataType(vectorDataType))
default:
dataType, err := stringBuffer.ReadString(',')
if err == nil {
dataType = dataType[:len(dataType)-1]
}
dataType = strings.TrimSpace(dataType)
dataTypes = append(dataTypes, DataType(dataType))
}
}

return dataTypes, nil
}
49 changes: 49 additions & 0 deletions pkg/sdk/identifier_parsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ func Test_ParseIdentifierString(t *testing.T) {
require.ErrorContains(t, err, `unable to parse identifier: "ab""c".def, currently identifiers containing double quotes are not supported in the provider`)
})

t.Run("returns error when identifier contains opening parenthesis", func(t *testing.T) {
input := `"ab(c".def`
_, err := ParseIdentifierString(input)

require.ErrorContains(t, err, `unable to parse identifier: "ab(c".def, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`)
})

t.Run("returns error when identifier contains closing parenthesis", func(t *testing.T) {
input := `"ab)c".def`
_, err := ParseIdentifierString(input)

require.ErrorContains(t, err, `unable to parse identifier: "ab)c".def, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`)
})

t.Run("returns error when identifier contains opening and closing parentheses", func(t *testing.T) {
input := `"ab()c".def`
_, err := ParseIdentifierString(input)

require.ErrorContains(t, err, `unable to parse identifier: "ab()c".def, currently identifiers containing opening and closing parentheses '()' are not supported in the provider`)
})

t.Run("returns parts correctly with dots inside", func(t *testing.T) {
input := `"ab.c".def`
expected := []string{`ab.c`, "def"}
Expand Down Expand Up @@ -250,3 +271,31 @@ func Test_ParseObjectIdentifierString(t *testing.T) {
})
}
}

func Test_ParseFunctionArgumentsFromString(t *testing.T) {
testCases := []struct {
Arguments string
Expected []DataType
Error string
}{
{Arguments: `()`, Expected: []DataType{}},
{Arguments: `(FLOAT, NUMBER, TIME)`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataTypeTime}},
{Arguments: `FLOAT, NUMBER, TIME`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataTypeTime}},
{Arguments: `(FLOAT, NUMBER, VECTOR(FLOAT, 20))`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataType("VECTOR(FLOAT, 20)")}},
{Arguments: `FLOAT, NUMBER, VECTOR(FLOAT, 20)`, Expected: []DataType{DataTypeFloat, DataTypeNumber, DataType("VECTOR(FLOAT, 20)")}},
{Arguments: `(VECTOR(FLOAT, 10), NUMBER, VECTOR(FLOAT, 20))`, Expected: []DataType{DataType("VECTOR(FLOAT, 10)"), DataTypeNumber, DataType("VECTOR(FLOAT, 20)")}},
{Arguments: `VECTOR(FLOAT, 10)| NUMBER, VECTOR(FLOAT, 20)`, Error: "expected a comma delimited string but found |"},
}

for _, testCase := range testCases {
t.Run(fmt.Sprintf("parsing function arguments %s", testCase.Arguments), func(t *testing.T) {
dataTypes, err := ParseFunctionArgumentsFromString(testCase.Arguments)
if testCase.Error != "" {
assert.ErrorContains(t, err, testCase.Error)
} else {
assert.NoError(t, err)
assert.Equal(t, testCase.Expected, dataTypes)
}
})
}
}
Loading

0 comments on commit 5767204

Please sign in to comment.