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

[feature] table resource #242

Merged
merged 21 commits into from
Oct 7, 2020
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ For the Terraform resources, there are 3 levels of testing - internal, unit and

The 'internal' tests are run in the `github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources` package so that they can test functions that are not exported. These tests are intended to be limited to unit tests for simple functions.

The 'unit' tests are run in `github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources_test`, so they only have access to the exported methods of `resources`. These tests exercise the CRUD methods that on the terraform resources. Note that all tests here make use of database mocking and are run locally. This means the tests are fast, but are liable to be wrong in suble ways (since the mocks are unlikely to be perfect).
The 'unit' tests are run in `github.com/chanzuckerberg/terraform-provider-snowflake/pkg/resources_test`, so they only have access to the exported methods of `resources`. These tests exercise the CRUD methods that on the terraform resources. Note that all tests here make use of database mocking and are run locally. This means the tests are fast, but are liable to be wrong in subtle ways (since the mocks are unlikely to be perfect).

You can run these first two sets of tests with `make test`.

Expand Down
15 changes: 15 additions & 0 deletions docs/resources/table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

# snowflake_table

<!-- These docs are auto-generated by code in ./docgen, run by with make docs. Manual edits will be overwritten. -->

## properties

| NAME | TYPE | DESCRIPTION | OPTIONAL | REQUIRED | COMPUTED | DEFAULT |
|----------|--------|-------------------------------------------------------------------------------------------------------------------|----------|-----------|----------|---------|
| column | list | Definitions of a column to create in the table. Minimum one required. | false | true | false | |
| comment | string | Specifies a comment for the table. | true | false | false | |
| database | string | The database in which to create the table. | false | true | false | |
| name | string | Specifies the identifier for the table; must be unique for the database and schema in which the table is created. | false | true | false | |
| owner | string | Name of the role that owns the table. | false | false | true | |
| schema | string | The schema in which to create the table. | false | true | false | |
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func Provider() *schema.Provider {
"snowflake_view": resources.View(),
"snowflake_view_grant": resources.ViewGrant(),
"snowflake_task": resources.Task(),
"snowflake_table": resources.Table(),
"snowflake_table_grant": resources.TableGrant(),
"snowflake_warehouse": resources.Warehouse(),
"snowflake_warehouse_grant": resources.WarehouseGrant(),
Expand Down
8 changes: 8 additions & 0 deletions pkg/resources/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ func storageIntegration(t *testing.T, id string, params map[string]interface{})
return d
}

func table(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.Table().Schema, params)
r.NotNil(d)
d.SetId(id)
return d
}

func user(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.User().Schema, params)
Expand Down
287 changes: 287 additions & 0 deletions pkg/resources/table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package resources

import (
"bytes"
"database/sql"
"encoding/csv"
"fmt"
"strings"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/pkg/errors"
)

const (
tableIDDelimiter = '|'
)

var tableSchema = map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Specifies the identifier for the table; must be unique for the database and schema in which the table is created.",
},
"schema": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The schema in which to create the table.",
},
"database": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The database in which to create the table.",
},
"column": {
Type: schema.TypeList,
Required: true,
MinItems: 1,
ForceNew: true,
Description: "Definitions of a column to create in the table. Minimum one required.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
Description: "Column name",
},
"type": {
Type: schema.TypeString,
Required: true,
Description: "Column type, e.g. VARIANT",
},
},
},
},
"comment": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies a comment for the table.",
},
"owner": {
Type: schema.TypeString,
Computed: true,
Description: "Name of the role that owns the table.",
},
}

func Table() *schema.Resource {
return &schema.Resource{
Create: CreateTable,
Read: ReadTable,
Update: UpdateTable,
Delete: DeleteTable,
Exists: TableExists,

Schema: tableSchema,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
}
}

type tableID struct {
DatabaseName string
SchemaName string
TableName string
}

//String() takes in a tableID object and returns a pipe-delimited string:
//DatabaseName|SchemaName|TableName
func (si *tableID) String() (string, error) {
var buf bytes.Buffer
csvWriter := csv.NewWriter(&buf)
csvWriter.Comma = tableIDDelimiter
dataIdentifiers := [][]string{{si.DatabaseName, si.SchemaName, si.TableName}}
err := csvWriter.WriteAll(dataIdentifiers)
if err != nil {
return "", err
}
strTableID := strings.TrimSpace(buf.String())
return strTableID, nil
}

// tableIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|TableName
// and returns a tableID object
func tableIDFromString(stringID string) (*tableID, error) {
reader := csv.NewReader(strings.NewReader(stringID))
reader.Comma = tableIDDelimiter
lines, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("Not CSV compatible")
}

if len(lines) != 1 {
return nil, fmt.Errorf("1 line at a time")
}
if len(lines[0]) != 3 {
return nil, fmt.Errorf("3 fields allowed")
}

tableResult := &tableID{
DatabaseName: lines[0][0],
SchemaName: lines[0][1],
TableName: lines[0][2],
}
return tableResult, nil
}

// CreateTable implements schema.CreateFunc
func CreateTable(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
database := data.Get("database").(string)
schema := data.Get("schema").(string)
name := data.Get("name").(string)

// This type conversion is due to the test framework in the terraform-plugin-sdk having limited support
// for data types in the HCL2ValueFromConfigValue method.
columns := []map[string]string{}
for _, column := range data.Get("column").([]interface{}) {
columnDef := map[string]string{}
for key, val := range column.(map[string]interface{}) {
columnDef[key] = val.(string)
}
columns = append(columns, columnDef)
}
builder := snowflake.TableWithColumnDefinitions(name, database, schema, columns)

// Set optionals
if v, ok := data.GetOk("comment"); ok {
builder.WithComment(v.(string))
}

stmt := builder.Create()
err := snowflake.Exec(db, stmt)
if err != nil {
return errors.Wrapf(err, "error creating table %v", name)
}

tableID := &tableID{
DatabaseName: database,
SchemaName: schema,
TableName: name,
}
dataIDInput, err := tableID.String()
if err != nil {
return err
}
data.SetId(dataIDInput)

return ReadTable(data, meta)
}

// ReadTable implements schema.ReadFunc
func ReadTable(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
tableID, err := tableIDFromString(data.Id())
if err != nil {
return err
}

dbName := tableID.DatabaseName
schema := tableID.SchemaName
name := tableID.TableName

stmt := snowflake.Table(name, dbName, schema).Show()
row := snowflake.QueryRow(db, stmt)
table, err := snowflake.ScanTable(row)
if err != nil {
return err
}

err = data.Set("name", table.TableName.String)
if err != nil {
return err
}

err = data.Set("owner", table.Owner.String)
if err != nil {
return err
}

return nil
}

// UpdateTable implements schema.UpdateFunc
func UpdateTable(data *schema.ResourceData, meta interface{}) error {
// https://www.terraform.io/docs/extend/writing-custom-providers.html#error-handling-amp-partial-state
data.Partial(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this goes in after #262, we need to remove partial usage.


tableID, err := tableIDFromString(data.Id())
if err != nil {
return err
}

dbName := tableID.DatabaseName
schema := tableID.SchemaName
tableName := tableID.TableName

builder := snowflake.Table(tableName, dbName, schema)

db := meta.(*sql.DB)
if data.HasChange("comment") {
_, comment := data.GetChange("comment")
q := builder.ChangeComment(comment.(string))
err := snowflake.Exec(db, q)
if err != nil {
return errors.Wrapf(err, "error updating table comment on %v", data.Id())
}

data.SetPartial("comment")
}

return ReadTable(data, meta)
}

// DeleteTable implements schema.DeleteFunc
func DeleteTable(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
tableID, err := tableIDFromString(data.Id())
if err != nil {
return err
}

dbName := tableID.DatabaseName
schema := tableID.SchemaName
tableName := tableID.TableName

q := snowflake.Table(tableName, dbName, schema).Drop()

err = snowflake.Exec(db, q)
if err != nil {
return errors.Wrapf(err, "error deleting pipe %v", data.Id())
}

data.SetId("")

return nil
}

// TableExists implements schema.ExistsFunc
func TableExists(data *schema.ResourceData, meta interface{}) (bool, error) {
db := meta.(*sql.DB)
tableID, err := tableIDFromString(data.Id())
if err != nil {
return false, err
}

dbName := tableID.DatabaseName
schema := tableID.SchemaName
tableName := tableID.TableName

q := snowflake.Table(tableName, dbName, schema).Show()
rows, err := db.Query(q)
if err != nil {
return false, err
}
defer rows.Close()

if rows.Next() {
return true, nil
}

return false, nil
}
59 changes: 59 additions & 0 deletions pkg/resources/table_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package resources_test

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccTable(t *testing.T) {
accName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.Test(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: tableConfig(accName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_table.test_table", "name", accName),
resource.TestCheckResourceAttr("snowflake_table.test_table", "database", accName),
resource.TestCheckResourceAttr("snowflake_table.test_table", "schema", accName),
resource.TestCheckResourceAttr("snowflake_table.test_table", "comment", "Terraform acceptance test"),
),
},
},
})
}

func tableConfig(name string) string {
s := `
resource "snowflake_database" "test_database" {
name = "%s"
comment = "Terraform acceptance test"
}

resource "snowflake_schema" "test_schema" {
name = "%s"
database = snowflake_database.test_database.name
comment = "Terraform acceptance test"
}

resource "snowflake_table" "test_table" {
database = snowflake_database.test_database.name
schema = snowflake_schema.test_schema.name
name = "%s"
comment = "Terraform acceptance test"
column {
name = "column1"
type = "VARIANT"
}
column {
name = "column2"
type = "VARCHAR"
}
}
`
return fmt.Sprintf(s, name, name, name)
}
Loading