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: Add identifier with arguments #2979

Merged
merged 14 commits into from
Aug 8, 2024
Prev Previous commit
Next Next commit
wip
sfc-gh-jcieslak committed Aug 5, 2024
commit 39be3822afa98f70f65f95e2783b45c1a573f4c1
25 changes: 14 additions & 11 deletions pkg/acceptance/check_destroy.go
Original file line number Diff line number Diff line change
@@ -27,7 +27,10 @@ func CheckDestroy(t *testing.T, resource resources.Resource) func(*terraform.Sta
}
t.Logf("found resource %s in state", resource)
ctx := context.Background()
id := decodeSnowflakeId(rs, resource)
id, err := decodeSnowflakeId(rs, resource)
if err != nil {
return err
}
if id == nil {
return fmt.Errorf("could not get the id of %s", resource)
}
@@ -45,16 +48,16 @@ func CheckDestroy(t *testing.T, resource resources.Resource) func(*terraform.Sta
}
}

func decodeSnowflakeId(rs *terraform.ResourceState, resource resources.Resource) sdk.ObjectIdentifier {
func decodeSnowflakeId(rs *terraform.ResourceState, resource resources.Resource) (sdk.ObjectIdentifier, error) {
switch resource {
case resources.ExternalFunction:
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID)
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID), nil
case resources.Function:
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID)
return sdk.NewSchemaObjectIdentifierWithArgumentsFromFullyQualifiedName(rs.Primary.ID)
case resources.Procedure:
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID)
return sdk.NewSchemaObjectIdentifierFromFullyQualifiedName(rs.Primary.ID), nil
default:
return helpers.DecodeSnowflakeID(rs.Primary.ID)
return helpers.DecodeSnowflakeID(rs.Primary.ID), nil
}
}

@@ -115,9 +118,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{
resources.FileFormat: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.FileFormats.ShowByID)
},
//resources.Function: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
// return runShowById(ctx, id, client.Functions.ShowByID)
//},
resources.Function: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.Functions.ShowByID)
},
resources.ManagedAccount: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ManagedAccounts.ShowByID)
},
@@ -213,7 +216,7 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{
},
}

func runShowById[T any, U sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier](ctx context.Context, id sdk.ObjectIdentifier, show func(ctx context.Context, id U) (T, error)) error {
func runShowById[T any, U sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier | sdk.SchemaObjectIdentifierWithArguments](ctx context.Context, id sdk.ObjectIdentifier, show func(ctx context.Context, id U) (T, error)) error {
idCast, err := asId[U](id)
if err != nil {
return err
@@ -222,7 +225,7 @@ func runShowById[T any, U sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdenti
return err
}

func asId[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier](id sdk.ObjectIdentifier) (*T, error) {
func asId[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier | sdk.SchemaObjectIdentifierWithArguments](id sdk.ObjectIdentifier) (*T, error) {
if idCast, ok := id.(T); !ok {
return nil, fmt.Errorf("expected %s identifier type, but got: %T", reflect.TypeOf(new(T)).Elem().Name(), id)
} else {
2 changes: 1 addition & 1 deletion pkg/datasources/functions.go
Original file line number Diff line number Diff line change
@@ -92,7 +92,7 @@ func ReadContextFunctions(ctx context.Context, d *schema.ResourceData, meta inte

entities := []map[string]interface{}{}
for _, item := range functions {
signature, err := parseArguments(item.Arguments)
signature, err := parseArguments(item.ArgumentsRaw)
if err != nil {
return diag.FromErr(err)
}
10 changes: 5 additions & 5 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
@@ -437,11 +437,11 @@ func getResources() map[string]*schema.Resource {
"snowflake_dynamic_table": resources.DynamicTable(),
"snowflake_email_notification_integration": resources.EmailNotificationIntegration(),
//"snowflake_external_function": resources.ExternalFunction(),
"snowflake_external_oauth_integration": resources.ExternalOauthIntegration(),
"snowflake_external_table": resources.ExternalTable(),
"snowflake_failover_group": resources.FailoverGroup(),
"snowflake_file_format": resources.FileFormat(),
//"snowflake_function": resources.Function(),
"snowflake_external_oauth_integration": resources.ExternalOauthIntegration(),
"snowflake_external_table": resources.ExternalTable(),
"snowflake_failover_group": resources.FailoverGroup(),
"snowflake_file_format": resources.FileFormat(),
"snowflake_function": resources.Function(),
"snowflake_grant_account_role": resources.GrantAccountRole(),
"snowflake_grant_application_role": resources.GrantApplicationRole(),
"snowflake_grant_database_role": resources.GrantDatabaseRole(),
1,528 changes: 773 additions & 755 deletions pkg/resources/function.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/resources/function_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -185,7 +185,7 @@ func TestAcc_Function_complex(t *testing.T) {

// proves issue https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2490
func TestAcc_Function_migrateFromVersion085(t *testing.T) {
id := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithArguments([]sdk.DataType{sdk.DataTypeVARCHAR})
id := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithArguments(sdk.DataTypeVARCHAR)
name := id.Name()
comment := random.Comment()
resourceName := "snowflake_function.f"
110 changes: 58 additions & 52 deletions pkg/resources/function_state_upgraders.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,60 @@
package resources

//type v085FunctionId struct {
// DatabaseName string
// SchemaName string
// FunctionName string
// ArgTypes []string
//}
//
//func parseV085FunctionId(v string) (*v085FunctionId, error) {
// arr := strings.Split(v, "|")
// if len(arr) != 4 {
// return nil, sdk.NewError(fmt.Sprintf("ID %v is invalid", v))
// }
//
// // this is a bit different from V085 state, but it was buggy
// var args []string
// if arr[3] != "" {
// args = strings.Split(arr[3], "-")
// }
//
// return &v085FunctionId{
// DatabaseName: arr[0],
// SchemaName: arr[1],
// FunctionName: arr[2],
// ArgTypes: args,
// }, nil
//}
//
//func v085FunctionIdStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
// if rawState == nil {
// return rawState, nil
// }
//
// oldId := rawState["id"].(string)
// parsedV085FunctionId, err := parseV085FunctionId(oldId)
// if err != nil {
// return nil, err
// }
//
// argDataTypes := make([]sdk.DataType, len(parsedV085FunctionId.ArgTypes))
// for i, argType := range parsedV085FunctionId.ArgTypes {
// argDataType, err := sdk.ToDataType(argType)
// if err != nil {
// return nil, err
// }
// argDataTypes[i] = argDataType
// }
//
// schemaObjectIdentifierWithArguments := sdk.NewSchemaObjectIdentifierWithArguments(parsedV085FunctionId.DatabaseName, parsedV085FunctionId.SchemaName, parsedV085FunctionId.FunctionName, argDataTypes)
// rawState["id"] = schemaObjectIdentifierWithArguments.FullyQualifiedName()
//
// return rawState, nil
//}
import (
"context"
"fmt"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk"
"strings"
)

type v085FunctionId struct {
DatabaseName string
SchemaName string
FunctionName string
ArgTypes []string
}

func parseV085FunctionId(v string) (*v085FunctionId, error) {
arr := strings.Split(v, "|")
if len(arr) != 4 {
return nil, sdk.NewError(fmt.Sprintf("ID %v is invalid", v))
}

// this is a bit different from V085 state, but it was buggy
var args []string
if arr[3] != "" {
args = strings.Split(arr[3], "-")
}

return &v085FunctionId{
DatabaseName: arr[0],
SchemaName: arr[1],
FunctionName: arr[2],
ArgTypes: args,
}, nil
}
func v085FunctionIdStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) {
if rawState == nil {
return rawState, nil
}

oldId := rawState["id"].(string)
parsedV085FunctionId, err := parseV085FunctionId(oldId)
if err != nil {
return nil, err
}

argDataTypes := make([]sdk.DataType, len(parsedV085FunctionId.ArgTypes))
for i, argType := range parsedV085FunctionId.ArgTypes {
argDataType, err := sdk.ToDataType(argType)
if err != nil {
return nil, err
}
argDataTypes[i] = argDataType
}

schemaObjectIdentifierWithArguments := sdk.NewSchemaObjectIdentifierWithArguments(parsedV085FunctionId.DatabaseName, parsedV085FunctionId.SchemaName, parsedV085FunctionId.FunctionName, argDataTypes...)
rawState["id"] = schemaObjectIdentifierWithArguments.FullyQualifiedName()

return rawState, nil
}
763 changes: 374 additions & 389 deletions pkg/resources/procedure_acceptance_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/schemas/function_gen.go
1 change: 1 addition & 0 deletions pkg/schemas/gen/main/main.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ package main

import (
"fmt"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/genhelpers"
"os"
"slices"
"strings"
5 changes: 5 additions & 0 deletions pkg/sdk/data_types.go
Original file line number Diff line number Diff line change
@@ -89,6 +89,11 @@ func ToDataType(s string) (DataType, error) {
return DataTypeTime, nil
}

vectorSynonyms := []string{"VECTOR"}
sfc-gh-jmichalak marked this conversation as resolved.
Show resolved Hide resolved
if slices.ContainsFunc(vectorSynonyms, func(e string) bool { return strings.HasPrefix(dType, e) }) {
return DataType(dType), nil
}

return "", fmt.Errorf("invalid data type: %s", s)
}

59 changes: 46 additions & 13 deletions pkg/sdk/functions_gen.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ package sdk
import (
"context"
"database/sql"
"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections"
"strings"
)

type Functions interface {
@@ -221,15 +223,16 @@ type functionRow struct {
}

type Function struct {
CreatedOn string
Name string
SchemaName string
IsBuiltin bool
IsAggregate bool
IsAnsi bool
MinNumArguments int
MaxNumArguments int
Arguments string
CreatedOn string
Name string
SchemaName string
IsBuiltin bool
IsAggregate bool
IsAnsi bool
MinNumArguments int
MaxNumArguments int
// TODO(SNOW-function refactor): Remove raw arguments
ArgumentsRaw string
Description string
CatalogName string
IsTableFunction bool
@@ -240,10 +243,40 @@ type Function struct {
IsMemoizable bool
}

func (v *Function) ID() SchemaObjectIdentifierWithArguments {
// return NewSchemaObjectIdentifier(v.CatalogName, v.SchemaName, v.Name)
// TODO:
return SchemaObjectIdentifierWithArguments{}
// parseFunctionArgumentsFromDetails parses arguments from signature that is contained in function details
func parseFunctionArgumentsFromDetails(details []FunctionDetail) ([]DataType, error) {
signatureProperty, err := collections.FindOne(details, func(detail FunctionDetail) bool { return detail.Property == "signature" })
if err != nil {
return nil, err
}
// signature has a structure of (<column name> <data type>, ...); column names can contain commas and other special characters,
// and they're not escaped, meaning for names such as `"a,b.c|d e" NUMBER` the signature will contain `(a,b.c|d e NUMBER)`.
arguments := make([]DataType, 0)
// TODO(TODO - ticket number): Handle arguments with comma in the name (right now this could break for arguments containing dots)
for _, arg := range strings.Split(strings.Trim(signatureProperty.Value, "()"), ",") {
// single argument has a structure of <column name> <data type>
argumentSignatureParts := strings.Split(strings.TrimSpace(arg), " ")
arguments = append(arguments, DataType(argumentSignatureParts[len(argumentSignatureParts)-1]))
}
return arguments, nil
}

func parseFunctionArgumentsFromString(arguments string) []DataType {
// TODO what about data types with parentheses
argListBegin := strings.LastIndex(arguments, "(")
argListEnd := strings.LastIndex(arguments, ")")
return collections.Map(
ParseCommaSeparatedStringArray(arguments[argListBegin+1:argListEnd], false),
func(dataType string) DataType { return DataType(dataType) },
)
}

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
}

// DescribeFunctionOptions is based on https://docs.snowflake.com/en/sql-reference/sql/desc-function.
4 changes: 2 additions & 2 deletions pkg/sdk/functions_impl_gen.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package sdk

import (
"context"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/internal/collections"
)

@@ -63,6 +62,7 @@ func (v *functions) ShowByID(ctx context.Context, id SchemaObjectIdentifierWithA
if err != nil {
return nil, err
}
// TODO: Should compare arguments
return collections.FindOne(functions, func(r Function) bool { return r.Name == id.Name() })
}

@@ -371,7 +371,7 @@ func (r functionRow) convert() *Function {
IsAnsi: r.IsAnsi == "Y",
MinNumArguments: r.MinNumArguments,
MaxNumArguments: r.MaxNumArguments,
Arguments: r.Arguments,
ArgumentsRaw: r.Arguments,
Description: r.Description,
CatalogName: r.CatalogName,
IsTableFunction: r.IsTableFunction == "Y",
67 changes: 57 additions & 10 deletions pkg/sdk/identifier_helpers.go
Original file line number Diff line number Diff line change
@@ -312,16 +312,53 @@ func NewSchemaObjectIdentifierWithArgumentsInSchema(schemaId DatabaseObjectIdent
return NewSchemaObjectIdentifierWithArguments(schemaId.DatabaseName(), schemaId.Name(), name, argumentDataTypes...)
}

// TODO:
//func NewSchemaObjectIdentifierWithArgumentsFromFullyQualifiedName(fullyQualifiedName string) SchemaObjectIdentifierWithArguments {
// parts := strings.Split(fullyQualifiedName, ".")
// id := SchemaObjectIdentifierWithArguments{
// databaseName: strings.Trim(parts[0], `"`),
// schemaName: strings.Trim(parts[1], `"`),
// name: strings.Trim(parts[2], `"`),
// // TODO: Arguments
func NewSchemaObjectIdentifierWithArgumentsFromFullyQualifiedName(fullyQualifiedName string) (SchemaObjectIdentifierWithArguments, error) {
sfc-gh-jmichalak marked this conversation as resolved.
Show resolved Hide resolved
// TODO: This is wrong
argsBegin := strings.LastIndex(fullyQualifiedName, "(")
parts, err := parseIdentifierStringWithOpts(fullyQualifiedName[:argsBegin], func(r *csv.Reader) {
r.Comma = '.'
})
if err != nil {
return SchemaObjectIdentifierWithArguments{}, err
}
return NewSchemaObjectIdentifierWithArguments(
parts[0],
parts[1],
parts[2],
parseFunctionArgumentsFromString(fullyQualifiedName[argsBegin:])...,
), nil
}

// TODO: Remove this func
func parseIdentifierStringWithOpts(identifier string, opts func(*csv.Reader)) ([]string, error) {
reader := csv.NewReader(strings.NewReader(identifier))
if opts != nil {
opts(reader)
}
lines, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("unable to read identifier: %s, err = %w", identifier, err)
}
if lines == nil {
return make([]string, 0), nil
}
if len(lines) != 1 {
return nil, fmt.Errorf("incompatible identifier: %s", identifier)
}
return lines[0], nil
}

// TODO: Move to resource package (or use FullyQUalifiedName and NewFromFullyQualifiedName because it will be needed anyway for things like returned ids from SHOW GRANTS)
//func NewSchemaObjectIdentifierWithArgumentsFromResourceIdentifier(resourceId string) SchemaObjectIdentifierWithArguments {
// // TODO: use standard parsing method
// resourceIdParts := strings.Split(resourceId, "|")
// schemaObjectId := NewSchemaObjectIdentifierFromFullyQualifiedName(resourceIdParts[0])
// argumentSlice := resourceIdParts[1:]
// arguments := make([]DataType, len(argumentSlice))
// for i, argument := range argumentSlice {
// arguments[i] = DataType(argument)
// }
// return id
// return NewSchemaObjectIdentifierWithArguments(schemaObjectId.DatabaseName(), schemaObjectId.SchemaName(), schemaObjectId.Name(), arguments...)
//}

func (i SchemaObjectIdentifierWithArguments) DatabaseName() string {
@@ -356,9 +393,19 @@ func (i SchemaObjectIdentifierWithArguments) FullyQualifiedName() string {
if i.schemaName == "" && i.databaseName == "" && i.name == "" && len(i.argumentDataTypes) == 0 {
return ""
}
return fmt.Sprintf(`"%v"."%v"."%v"(%v)`, i.databaseName, i.schemaName, i.name, strings.Join(AsStringList(i.argumentDataTypes), ", "))
return fmt.Sprintf(`"%v"."%v"."%v"(%v)`, i.databaseName, i.schemaName, i.name, strings.Join(AsStringList(i.argumentDataTypes), ","))
}

// TODO: Move to resource package
//func (i SchemaObjectIdentifierWithArguments) AsResourceIdentifier() string {
// // TODO: use standard encoding method
// resourceId := []string{
// i.SchemaObjectId().FullyQualifiedName(),
// }
// resourceId = append(resourceId, AsStringList(i.ArgumentDataTypes())...)
// return strings.Join(resourceId, "|")
//}

type TableColumnIdentifier struct {
databaseName string
schemaName string
20 changes: 20 additions & 0 deletions pkg/sdk/identifier_helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sdk

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
@@ -83,3 +84,22 @@ func TestDatabaseObjectIdentifier(t *testing.T) {
assert.Equal(t, `"aaa"."bbb"`, identifier.FullyQualifiedName())
})
}

// TODO: test cases
func TestSchemaObjectIdentifierWithArguments(t *testing.T) {
t.Run("create new from fully qualified name", func(t *testing.T) {
testCases := []struct {
Input SchemaObjectIdentifierWithArguments
}{
{Input: NewSchemaObjectIdentifierWithArguments(`abc`, `def`, `ghi`, DataTypeFloat, DataTypeNumber, DataTypeTimestampTZ)},
}

for _, testCase := range testCases {
t.Run(fmt.Sprintf("processing %s", testCase.Input.FullyQualifiedName()), func(t *testing.T) {
parsedId, err := NewSchemaObjectIdentifierWithArgumentsFromFullyQualifiedName(testCase.Input.FullyQualifiedName())
require.NoError(t, err)
require.Equal(t, testCase.Input.FullyQualifiedName(), parsedId.FullyQualifiedName())
})
}
})
}
595 changes: 333 additions & 262 deletions pkg/sdk/testint/functions_integration_test.go

Large diffs are not rendered by default.