Skip to content

Commit

Permalink
feat: Add support for packages, imports, handler and `runtimeVe…
Browse files Browse the repository at this point in the history
…rsion` to `snowflake_procedure` resource (#1516)

* Initial copy-paste of missing procedure params from function

* Fix function renames; return type placement; allowed languages

* Update TestProcedureCreateWithOptionalParams with new optional parameters

* function → procedure in a few descriptions

* Remove whitespace in import strings; generated docs

- Whitespace in import strings can cause TF to believe that
  there are changes to the `snowflake_procedure` resource,
  as gaps in import names in the Snowflake response cause
  a space at the start of any imports after the first

---------

Co-authored-by: Matthew Badger <[email protected]>
  • Loading branch information
badge and Matthew Badger authored Mar 22, 2023
1 parent afb18aa commit a88f3ad
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 8 deletions.
6 changes: 5 additions & 1 deletion docs/resources/procedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,20 @@ EOT
- `name` (String) Specifies the identifier for the procedure; does not have to be unique for the schema in which the procedure is created. Don't use the | character.
- `return_type` (String) The return type of the procedure
- `schema` (String) The schema in which to create the procedure. Don't use the | character.
- `statement` (String) Specifies the javascript code used to create the procedure.
- `statement` (String) Specifies the code used to create the procedure.

### Optional

- `arguments` (Block List) List of the arguments for the procedure (see [below for nested schema](#nestedblock--arguments))
- `comment` (String) Specifies a comment for the procedure.
- `execute_as` (String) Sets execute context - see caller's rights and owner's rights
- `handler` (String) The handler method for Java / Python procedures.
- `imports` (List of String) Imports for Java / Python procedures. For Java this a list of jar files, for Python this is a list of Python files.
- `language` (String) Specifies the language of the stored procedure code.
- `null_input_behavior` (String) Specifies the behavior of the procedure when called with null inputs.
- `packages` (List of String) List of package imports to use for Java / Python procedures. For Java, package imports should be of the form: package_name:version_number, where package_name is snowflake_domain:package. For Python use it should be: ('numpy','pandas','xgboost==1.5.0').
- `return_behavior` (String) Specifies the behavior of the function when returning results
- `runtime_version` (String) Required for Python procedures. Specifies Python runtime version.

### Read-Only

Expand Down
86 changes: 84 additions & 2 deletions pkg/resources/procedure.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"golang.org/x/exp/slices"
)

var procedureLanguages = []string{"javascript", "java", "scala", "SQL"}
var procedureLanguages = []string{"javascript", "java", "scala", "SQL", "python"}

var procedureSchema = map[string]*schema.Schema{
"name": {
Expand Down Expand Up @@ -89,7 +89,7 @@ var procedureSchema = map[string]*schema.Schema{
"statement": {
Type: schema.TypeString,
Required: true,
Description: "Specifies the javascript code used to create the procedure.",
Description: "Specifies the code used to create the procedure.",
ForceNew: true,
DiffSuppressFunc: DiffSuppressStatement,
},
Expand Down Expand Up @@ -132,6 +132,36 @@ var procedureSchema = map[string]*schema.Schema{
Default: "user-defined procedure",
Description: "Specifies a comment for the procedure.",
},
"runtime_version": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "Required for Python procedures. Specifies Python runtime version.",
},
"packages": {
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Optional: true,
ForceNew: true,
Description: "List of package imports to use for Java / Python procedures. For Java, package imports should be of the form: package_name:version_number, where package_name is snowflake_domain:package. For Python use it should be: ('numpy','pandas','xgboost==1.5.0').",
},
"imports": {
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Optional: true,
ForceNew: true,
Description: "Imports for Java / Python procedures. For Java this a list of jar files, for Python this is a list of Python files.",
},
"handler": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "The handler method for Java / Python procedures.",
},
}

func DiffTypes(k, old, new string, d *schema.ResourceData) bool {
Expand Down Expand Up @@ -197,10 +227,38 @@ func CreateProcedure(d *schema.ResourceData, meta interface{}) error {
builder.WithLanguage(strings.ToUpper(v.(string)))
}

// Set optionals, runtime version for Python
if v, ok := d.GetOk("runtime_version"); ok {
builder.WithRuntimeVersion(v.(string))
}

if v, ok := d.GetOk("comment"); ok {
builder.WithComment(v.(string))
}

// Set optionals, packages for Java / Python
if _, ok := d.GetOk("packages"); ok {
packages := []string{}
for _, pack := range d.Get("packages").([]interface{}) {
packages = append(packages, pack.(string))
}
builder.WithPackages(packages)
}

// Set optionals, imports for Java / Python
if _, ok := d.GetOk("imports"); ok {
imports := []string{}
for _, imp := range d.Get("imports").([]interface{}) {
imports = append(imports, imp.(string))
}
builder.WithImports(imports)
}

// handler for Java / Python
if v, ok := d.GetOk("handler"); ok {
builder.WithHandler(v.(string))
}

q, err := builder.Create()
if err != nil {
return err
Expand Down Expand Up @@ -297,6 +355,30 @@ func ReadProcedure(d *schema.ResourceData, meta interface{}) error {
if err := d.Set("language", desc.Value.String); err != nil {
return err
}
case "runtime_version":
if err := d.Set("runtime_version", desc.Value.String); err != nil {
return err
}
case "packages":
packagesString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value.String, "[", ""), "]", ""), "'", "")
if packagesString != "" { // Do nothing for Java / Python functions without packages
packages := strings.Split(packagesString, ",")
if err := d.Set("packages", packages); err != nil {
return err
}
}
case "imports":
importsString := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(desc.Value.String, "[", ""), "]", ""), "'", ""), " ", "")
if importsString != "" { // Do nothing for Java functions without imports
imports := strings.Split(importsString, ",")
if err := d.Set("imports", imports); err != nil {
return err
}
}
case "handler":
if err := d.Set("handler", desc.Value.String); err != nil {
return err
}

default:
log.Printf("[WARN] unexpected procedure property %v returned from Snowflake", desc.Property.String)
Expand Down
54 changes: 53 additions & 1 deletion pkg/snowflake/procedure.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ type ProcedureBuilder struct {
args []map[string]string
returnBehavior string // VOLATILE, IMMUTABLE
nullInputBehavior string // "CALLED ON NULL INPUT" or "RETURNS NULL ON NULL INPUT"
language string // SQL, JAVASCRIPT, JAVA, SCALA
returnType string
language string // SQL, JAVASCRIPT, JAVA, SCALA
packages []string
imports []string // for Java / Python imports
handler string // for Java / Python handler
executeAs string
comment string
statement string
runtimeVersion string // for Python runtime version
}

// QualifiedName prepends the db and schema and appends argument types.
Expand Down Expand Up @@ -89,6 +93,30 @@ func (pb *ProcedureBuilder) WithLanguage(s string) *ProcedureBuilder {
return pb
}

// WithRuntimeVersion.
func (pb *ProcedureBuilder) WithRuntimeVersion(r string) *ProcedureBuilder {
pb.runtimeVersion = r
return pb
}

// WithPackages.
func (pb *ProcedureBuilder) WithPackages(s []string) *ProcedureBuilder {
pb.packages = s
return pb
}

// WithImports adds jar files to import for Java function or Python file for Python function.
func (pb *ProcedureBuilder) WithImports(s []string) *ProcedureBuilder {
pb.imports = s
return pb
}

// WithHandler sets the handler method for Java / Python function.
func (pb *ProcedureBuilder) WithHandler(s string) *ProcedureBuilder {
pb.handler = s
return pb
}

// WithComment adds a comment to the ProcedureBuilder.
func (pb *ProcedureBuilder) WithComment(c string) *ProcedureBuilder {
pb.comment = c
Expand Down Expand Up @@ -156,6 +184,30 @@ func (pb *ProcedureBuilder) Create() (string, error) {
if pb.returnBehavior != "" {
q.WriteString(fmt.Sprintf(` %v`, EscapeString(pb.returnBehavior)))
}
if pb.runtimeVersion != "" {
q.WriteString(fmt.Sprintf(" RUNTIME_VERSION = '%v'", EscapeString(pb.runtimeVersion)))
}
if len(pb.packages) > 0 {
q.WriteString(` PACKAGES = (`)
packages := []string{}
for _, pack := range pb.packages {
packages = append(packages, fmt.Sprintf(`'%v'`, pack))
}
q.WriteString(strings.Join(packages, ", "))
q.WriteString(`)`)
}
if len(pb.imports) > 0 {
q.WriteString(` IMPORTS = (`)
imports := []string{}
for _, imp := range pb.imports {
imports = append(imports, fmt.Sprintf(`'%v'`, imp))
}
q.WriteString(strings.Join(imports, ", "))
q.WriteString(`)`)
}
if pb.handler != "" {
q.WriteString(fmt.Sprintf(" HANDLER = '%v'", pb.handler))
}
if pb.comment != "" {
q.WriteString(fmt.Sprintf(" COMMENT = '%v'", EscapeString(pb.comment)))
}
Expand Down
13 changes: 9 additions & 4 deletions pkg/snowflake/procedure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,18 @@ func TestProcedureCreateWithOptionalParams(t *testing.T) {
s := getProcedure(true)
s.WithNullInputBehavior("RETURNS NULL ON NULL INPUT")
s.WithReturnBehavior("IMMUTABLE")
s.WithLanguage("JAVASCRIPT")
s.WithLanguage("PYTHON")
s.WithRuntimeVersion("3.8")
s.WithPackages([]string{"snowflake-snowpark-python", "pandas"})
s.WithImports([]string{"@\"test_db\".\"test_schema\".\"test_stage\"/handler.py"})
s.WithHandler("handler.test")
s.WithComment("this is cool proc!")
createStmnt, _ := s.Create()
expected := `CREATE OR REPLACE PROCEDURE "test_db"."test_schema"."test_proc"` +
`(user VARCHAR, eventdt DATE) RETURNS VARCHAR LANGUAGE JAVASCRIPT RETURNS NULL ON NULL INPUT` +
` IMMUTABLE COMMENT = 'this is cool proc!' EXECUTE AS CALLER AS $$` +
`var message = "Hi"` + "\nreturn message$$"
`(user VARCHAR, eventdt DATE) RETURNS VARCHAR LANGUAGE PYTHON RETURNS NULL ON NULL INPUT ` +
`IMMUTABLE RUNTIME_VERSION = '3.8' PACKAGES = ('snowflake-snowpark-python', 'pandas') ` +
`IMPORTS = ('@"test_db"."test_schema"."test_stage"/handler.py') HANDLER = 'handler.test' ` +
`COMMENT = 'this is cool proc!' EXECUTE AS CALLER AS $$var message = "Hi"` + "\nreturn message$$"
r.Equal(expected, createStmnt)
}

Expand Down

0 comments on commit a88f3ad

Please sign in to comment.