Repository: chanzuckerberg/terraform-provider-snowflake
Pull request #380 (2020-12-23 19:34:24)

simplify id codec code
Refactor to simplify and DRY up our code for generating IDs.

## Test Plan
* [ ] acceptance tests



Patch:
diff --git a/docs/resources/role_grants.md b/docs/resources/role_grants.md
index 371e2086..8cd90ddf 100644
--- a/docs/resources/role_grants.md
+++ b/docs/resources/role_grants.md
@@ -32,6 +32,8 @@ resource "snowflake_role" "other_role" {
 }
 
 resource "snowflake_role_grants" "grants" {
+  name = "foo"
+
   role_name = "${snowflake_role.role.name}"
 
   roles = [
diff --git a/go.mod b/go.mod
index 8f069057..8d3c0715 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/hashicorp/terraform-plugin-docs v0.2.0
 	github.com/hashicorp/terraform-plugin-sdk/v2 v2.3.0
 	github.com/jmoiron/sqlx v1.2.0
+	github.com/jszwec/csvutil v1.4.0
 	github.com/luna-duclos/instrumentedsql v1.1.3
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index 5a94ae71..e691244b 100644
--- a/go.sum
+++ b/go.sum
@@ -456,6 +456,8 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jszwec/csvutil v1.4.0 h1:ro7gZN8PRsyNUEX8qE/eYPE5/kffEXMs+4eRcOd1oUk=
+github.com/jszwec/csvutil v1.4.0/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
 github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
diff --git a/pkg/resources/grant_helpers.go b/pkg/resources/grant_helpers.go
index 5ad66f84..fa1eb6b3 100644
--- a/pkg/resources/grant_helpers.go
+++ b/pkg/resources/grant_helpers.go
@@ -1,9 +1,7 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
 	"fmt"
 	"regexp"
 	"strings"
@@ -31,10 +29,6 @@ func (t TerraformGrantResources) GetTfSchemas() map[string]*schema.Resource {
 	return out
 }
 
-const (
-	grantIDDelimiter = '|'
-)
-
 // currentGrant represents a generic grant of a privilege from a grant (the target) to a
 // grantee. This type can be used in conjunction with github.com/jmoiron/sqlx to
 // build a nice go representation of a grant
@@ -73,63 +67,6 @@ type grant struct {
 	GrantOption bool
 }
 
-// grantID contains identifying elements that allow unique access privileges
-type grantID struct {
-	ResourceName string
-	SchemaName   string
-	ObjectName   string
-	Privilege    string
-	GrantOption  bool
-}
-
-// String() takes in a grantID object and returns a pipe-delimited string:
-// resourceName|schemaName|ObjectName|Privilege|GrantOption
-func (gi *grantID) String() (string, error) {
-	var buf bytes.Buffer
-	csvWriter := csv.NewWriter(&buf)
-	csvWriter.Comma = grantIDDelimiter
-	grantOption := fmt.Sprintf("%v", gi.GrantOption)
-	dataIdentifiers := [][]string{{gi.ResourceName, gi.SchemaName, gi.ObjectName, gi.Privilege, grantOption}}
-	err := csvWriter.WriteAll(dataIdentifiers)
-	if err != nil {
-		return "", err
-	}
-	strGrantID := strings.TrimSpace(buf.String())
-	return strGrantID, nil
-}
-
-// grantIDFromString() takes in a pipe-delimited string: resourceName|schemaName|ObjectName|Privilege
-// and returns a grantID object
-func grantIDFromString(stringID string) (*grantID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = grantIDDelimiter
-	lines, err := reader.ReadAll()
-	if err != nil {
-		return nil, fmt.Errorf("Not CSV compatible")
-	}
-
-	if len(lines) != 1 {
-		return nil, fmt.Errorf("1 line per grant")
-	}
-	if len(lines[0]) != 4 && len(lines[0]) != 5 {
-		return nil, fmt.Errorf("4 or 5 fields allowed")
-	}
-
-	grantOption := false
-	if len(lines[0]) == 5 && lines[0][4] == "true" {
-		grantOption = true
-	}
-
-	grantResult := &grantID{
-		ResourceName: lines[0][0],
-		SchemaName:   lines[0][1],
-		ObjectName:   lines[0][2],
-		Privilege:    lines[0][3],
-		GrantOption:  grantOption,
-	}
-	return grantResult, nil
-}
-
 func createGenericGrant(d *schema.ResourceData, meta interface{}, builder snowflake.GrantBuilder) error {
 	db := meta.(*sql.DB)
 
diff --git a/pkg/resources/ids.go b/pkg/resources/ids.go
new file mode 100644
index 00000000..1205f36e
--- /dev/null
+++ b/pkg/resources/ids.go
@@ -0,0 +1,140 @@
+package resources
+
+import (
+	"bytes"
+	"encoding/csv"
+	"fmt"
+	"strings"
+
+	"github.com/jszwec/csvutil"
+)
+
+const (
+	delimiter         = '|'
+	streamOndelimiter = '.'
+)
+
+func writeID(in []string) (string, error) {
+	var buf bytes.Buffer
+	csvWriter := csv.NewWriter(&buf)
+	csvWriter.Comma = delimiter
+	err := csvWriter.WriteAll([][]string{in})
+	if err != nil {
+		return "", err
+	}
+	strGrantID := strings.TrimSpace(buf.String())
+	return strGrantID, nil
+}
+
+func readID(id string, data interface{}) error {
+	reader := csv.NewReader(strings.NewReader(id))
+	reader.Comma = delimiter
+
+	header, err := csvutil.Header(data, "csv")
+	if err != nil {
+		return err
+	}
+	fmt.Printf("[DEBUG] header %#v\n", header)
+	decoder, err := csvutil.NewDecoder(reader, header...)
+	if err != nil {
+		return err
+	}
+
+	return decoder.Decode(&data)
+}
+
+// grantID contains identifying elements that allow unique access privileges
+type grantID struct {
+	ResourceName string
+	SchemaName   string
+	ObjectName   string
+	Privilege    string
+	GrantOption  bool `csv:",omitempty"`
+}
+
+// String() takes in a grantID object and returns a pipe-delimited string:
+// resourceName|schemaName|ObjectName|Privilege|GrantOption
+func (gi *grantID) String() (string, error) {
+	grantOption := fmt.Sprintf("%v", gi.GrantOption)
+	return writeID([]string{gi.ResourceName, gi.SchemaName, gi.ObjectName, gi.Privilege, grantOption})
+}
+
+// grantIDFromString() takes in a pipe-delimited string: resourceName|schemaName|ObjectName|Privilege
+// and returns a grantID object
+func grantIDFromString(stringID string) (*grantID, error) {
+	result := &grantID{}
+	err := readID(stringID, result)
+	return result, err
+}
+
+type schemaID struct {
+	Database string
+	Name     string
+}
+
+// String() takes in a schemaID object and returns a pipe-delimited string:
+// DatabaseName|schemaName
+func (si *schemaID) String() (string, error) {
+	return writeID([]string{si.Database, si.Name})
+}
+
+// schemaIDFromString() takes in a pipe-delimited string: DatabaseName|schemaName
+// and returns a schemaID object
+func schemaIDFromString(stringID string) (*schemaID, error) {
+	result := &schemaID{}
+	err := readID(stringID, result)
+	return result, err
+}
+
+type streamOnTableID struct {
+	DatabaseName string
+	SchemaName   string
+	OnTableName  string
+}
+
+// streamOnTableIDFromString() takes in a dot-delimited string: DatabaseName.SchemaName.TableName
+// and returns a streamOnTableID object
+func streamOnTableIDFromString(stringID string) (*streamOnTableID, error) {
+	reader := csv.NewReader(strings.NewReader(stringID))
+	// TODO switch this to delimter, requires state transition
+	reader.Comma = streamOndelimiter
+	lines, err := reader.ReadAll()
+	if err != nil {
+		return nil, fmt.Errorf("Not CSV compatible")
+	}
+
+	if len(lines) != 1 {
+		return nil, fmt.Errorf("expecting 1 line")
+	}
+	if len(lines[0]) != 3 {
+		//return nil, fmt.Errorf("on table format: database_name.schema_name.target_table_name")
+		return nil, fmt.Errorf("invalid format for on_table: %v , expected: <database_name.schema_name.target_table_name>", strings.Join(lines[0], "."))
+	}
+
+	streamOnTableResult := &streamOnTableID{
+		DatabaseName: lines[0][0],
+		SchemaName:   lines[0][1],
+		OnTableName:  lines[0][2],
+	}
+	return streamOnTableResult, nil
+}
+
+type schemaScopedID struct {
+	Database string
+	Schema   string
+	Name     string
+}
+
+//String() takes in a schemaScopedID object and returns a pipe-delimited string:
+//DatabaseName|SchemaName|TaskName
+func (t *schemaScopedID) String() (string, error) {
+	return writeID([]string{t.Database, t.Schema, t.Name})
+}
+
+// taskIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|TaskName
+// and returns a taskID object
+func idFromString(stringID string) (*schemaScopedID, error) {
+	result := &schemaScopedID{}
+	err := readID(stringID, result)
+	return result, err
+}
diff --git a/pkg/resources/pipe.go b/pkg/resources/pipe.go
index 0ca523b5..f10d7df0 100644
--- a/pkg/resources/pipe.go
+++ b/pkg/resources/pipe.go
@@ -1,10 +1,7 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
-	"fmt"
 	"log"
 	"strings"
 
@@ -13,10 +10,6 @@ import (
 	"github.com/pkg/errors"
 )
 
-const (
-	pipeIDDelimiter = '|'
-)
-
 var pipeSchema = map[string]*schema.Schema{
 	"name": {
 		Type:        schema.TypeString,
@@ -95,52 +88,6 @@ func pipeCopyStatementDiffSuppress(k, old, new string, d *schema.ResourceData) b
 	return strings.TrimRight(old, ";\r\n") == strings.TrimRight(new, ";\r\n")
 }
 
-type pipeID struct {
-	DatabaseName string
-	SchemaName   string
-	PipeName     string
-}
-
-//String() takes in a pipeID object and returns a pipe-delimited string:
-//DatabaseName|SchemaName|PipeName
-func (si *pipeID) String() (string, error) {
-	var buf bytes.Buffer
-	csvWriter := csv.NewWriter(&buf)
-	csvWriter.Comma = pipeIDDelimiter
-	dataIdentifiers := [][]string{{si.DatabaseName, si.SchemaName, si.PipeName}}
-	err := csvWriter.WriteAll(dataIdentifiers)
-	if err != nil {
-		return "", err
-	}
-	strPipeID := strings.TrimSpace(buf.String())
-	return strPipeID, nil
-}
-
-// pipeIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|PipeName
-// and returns a pipeID object
-func pipeIDFromString(stringID string) (*pipeID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = pipeIDDelimiter
-	lines, err := reader.ReadAll()
-	if err != nil {
-		return nil, fmt.Errorf("Not CSV compatible")
-	}
-
-	if len(lines) != 1 {
-		return nil, fmt.Errorf("1 line per pipe")
-	}
-	if len(lines[0]) != 3 {
-		return nil, fmt.Errorf("3 fields allowed")
-	}
-
-	pipeResult := &pipeID{
-		DatabaseName: lines[0][0],
-		SchemaName:   lines[0][1],
-		PipeName:     lines[0][2],
-	}
-	return pipeResult, nil
-}
-
 // CreatePipe implements schema.CreateFunc
 func CreatePipe(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
@@ -174,10 +121,10 @@ func CreatePipe(d *schema.ResourceData, meta interface{}) error {
 		return errors.Wrapf(err, "error creating pipe %v", name)
 	}
 
-	pipeID := &pipeID{
-		DatabaseName: database,
-		SchemaName:   schema,
-		PipeName:     name,
+	pipeID := &schemaScopedID{
+		Database: database,
+		Schema:   schema,
+		Name:     name,
 	}
 	dataIDInput, err := pipeID.String()
 	if err != nil {
@@ -191,14 +138,14 @@ func CreatePipe(d *schema.ResourceData, meta interface{}) error {
 // ReadPipe implements schema.ReadFunc
 func ReadPipe(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	pipeID, err := pipeIDFromString(d.Id())
+	pipeID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := pipeID.DatabaseName
-	schema := pipeID.SchemaName
-	name := pipeID.PipeName
+	dbName := pipeID.Database
+	schema := pipeID.Schema
+	name := pipeID.Name
 
 	sq := snowflake.Pipe(name, dbName, schema).Show()
 	row := snowflake.QueryRow(db, sq)
@@ -263,14 +210,14 @@ func ReadPipe(d *schema.ResourceData, meta interface{}) error {
 
 // UpdatePipe implements schema.UpdateFunc
 func UpdatePipe(d *schema.ResourceData, meta interface{}) error {
-	pipeID, err := pipeIDFromString(d.Id())
+	pipeID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := pipeID.DatabaseName
-	schema := pipeID.SchemaName
-	pipe := pipeID.PipeName
+	dbName := pipeID.Database
+	schema := pipeID.Schema
+	pipe := pipeID.Name
 
 	builder := snowflake.Pipe(pipe, dbName, schema)
 
@@ -290,14 +237,14 @@ func UpdatePipe(d *schema.ResourceData, meta interface{}) error {
 // DeletePipe implements schema.DeleteFunc
 func DeletePipe(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	pipeID, err := pipeIDFromString(d.Id())
+	pipeID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := pipeID.DatabaseName
-	schema := pipeID.SchemaName
-	pipe := pipeID.PipeName
+	dbName := pipeID.Database
+	schema := pipeID.Schema
+	pipe := pipeID.Name
 
 	q := snowflake.Pipe(pipe, dbName, schema).Drop()
 
@@ -314,14 +261,14 @@ func DeletePipe(d *schema.ResourceData, meta interface{}) error {
 // PipeExists implements schema.ExistsFunc
 func PipeExists(data *schema.ResourceData, meta interface{}) (bool, error) {
 	db := meta.(*sql.DB)
-	pipeID, err := pipeIDFromString(data.Id())
+	pipeID, err := idFromString(data.Id())
 	if err != nil {
 		return false, err
 	}
 
-	dbName := pipeID.DatabaseName
-	schema := pipeID.SchemaName
-	pipe := pipeID.PipeName
+	dbName := pipeID.Database
+	schema := pipeID.Schema
+	pipe := pipeID.Name
 
 	q := snowflake.Pipe(pipe, dbName, schema).Show()
 	rows, err := db.Query(q)
diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go
index 9665b478..e9f36c54 100644
--- a/pkg/resources/schema.go
+++ b/pkg/resources/schema.go
@@ -1,10 +1,7 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
-	"fmt"
 	"log"
 	"strings"
 
@@ -15,10 +12,6 @@ import (
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 )
 
-const (
-	schemaIDDelimiter = '|'
-)
-
 var schemaSchema = map[string]*schema.Schema{
 	"name": {
 		Type:        schema.TypeString,
@@ -59,50 +52,6 @@ var schemaSchema = map[string]*schema.Schema{
 	},
 }
 
-type schemaID struct {
-	DatabaseName string
-	SchemaName   string
-}
-
-// String() takes in a schemaID object and returns a pipe-delimited string:
-// DatabaseName|schemaName
-func (si *schemaID) String() (string, error) {
-	var buf bytes.Buffer
-	csvWriter := csv.NewWriter(&buf)
-	csvWriter.Comma = schemaIDDelimiter
-	dataIdentifiers := [][]string{{si.DatabaseName, si.SchemaName}}
-	err := csvWriter.WriteAll(dataIdentifiers)
-	if err != nil {
-		return "", err
-	}
-	strSchemaID := strings.TrimSpace(buf.String())
-	return strSchemaID, nil
-}
-
-// schemaIDFromString() takes in a pipe-delimited string: DatabaseName|schemaName
-// and returns a schemaID object
-func schemaIDFromString(stringID string) (*schemaID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = schemaIDDelimiter
-	lines, err := reader.ReadAll()
-	if err != nil {
-		return nil, fmt.Errorf("Not CSV compatible")
-	}
-
-	if len(lines) != 1 {
-		return nil, fmt.Errorf("1 line per schema")
-	}
-	if len(lines[0]) != 2 {
-		return nil, fmt.Errorf("2 fields allowed")
-	}
-
-	schemaResult := &schemaID{
-		DatabaseName: lines[0][0],
-		SchemaName:   lines[0][1],
-	}
-	return schemaResult, nil
-}
-
 // Schema returns a pointer to the resource representing a schema
 func Schema() *schema.Resource {
 	return &schema.Resource{
@@ -151,8 +100,8 @@ func CreateSchema(d *schema.ResourceData, meta interface{}) error {
 	}
 
 	schemaID := &schemaID{
-		DatabaseName: database,
-		SchemaName:   name,
+		Database: database,
+		Name:     name,
 	}
 	dataIDInput, err := schemaID.String()
 	if err != nil {
@@ -171,8 +120,8 @@ func ReadSchema(d *schema.ResourceData, meta interface{}) error {
 		return err
 	}
 
-	dbName := schemaID.DatabaseName
-	schema := schemaID.SchemaName
+	dbName := schemaID.Database
+	schema := schemaID.Name
 
 	q := snowflake.Schema(schema).WithDB(dbName).Show()
 	row := snowflake.QueryRow(db, q)
@@ -246,8 +195,8 @@ func UpdateSchema(d *schema.ResourceData, meta interface{}) error {
 		return err
 	}
 
-	dbName := schemaID.DatabaseName
-	schema := schemaID.SchemaName
+	dbName := schemaID.Database
+	schema := schemaID.Name
 
 	builder := snowflake.Schema(schema).WithDB(dbName)
 
@@ -297,8 +246,8 @@ func DeleteSchema(d *schema.ResourceData, meta interface{}) error {
 		return err
 	}
 
-	dbName := schemaID.DatabaseName
-	schema := schemaID.SchemaName
+	dbName := schemaID.Database
+	schema := schemaID.Name
 
 	q := snowflake.Schema(schema).WithDB(dbName).Drop()
 
@@ -315,13 +264,13 @@ func DeleteSchema(d *schema.ResourceData, meta interface{}) error {
 // SchemaExists implements schema.ExistsFunc
 func SchemaExists(data *schema.ResourceData, meta interface{}) (bool, error) {
 	db := meta.(*sql.DB)
-	schemaID, err := schemaIDFromString(data.Id())
+	schemaID, err := idFromString(data.Id())
 	if err != nil {
 		return false, err
 	}
 
-	dbName := schemaID.DatabaseName
-	schema := schemaID.SchemaName
+	dbName := schemaID.Database
+	schema := schemaID.Name
 
 	q := snowflake.Schema(schema).WithDB(dbName).Show()
 	rows, err := db.Query(q)
diff --git a/pkg/resources/stage.go b/pkg/resources/stage.go
index ccec2098..76cbcc09 100644
--- a/pkg/resources/stage.go
+++ b/pkg/resources/stage.go
@@ -1,22 +1,14 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
-	"fmt"
 	"log"
-	"strings"
 
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/pkg/errors"
 )
 
-const (
-	stageIDDelimiter = '|'
-)
-
 var stageSchema = map[string]*schema.Schema{
 	"name": {
 		Type:        schema.TypeString,
@@ -84,52 +76,6 @@ var stageSchema = map[string]*schema.Schema{
 	},
 }
 
-type stageID struct {
-	DatabaseName string
-	SchemaName   string
-	StageName    string
-}
-
-// String() takes in a stageID object and returns a pipe-delimited string:
-// DatabaseName|SchemaName|StageName
-func (si *stageID) String() (string, error) {
-	var buf bytes.Buffer
-	csvWriter := csv.NewWriter(&buf)
-	csvWriter.Comma = stageIDDelimiter
-	dataIdentifiers := [][]string{{si.DatabaseName, si.SchemaName, si.StageName}}
-	err := csvWriter.WriteAll(dataIdentifiers)
-	if err != nil {
-		return "", err
-	}
-	strStageID := strings.TrimSpace(buf.String())
-	return strStageID, nil
-}
-
-// stageIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|StageName
-// and returns a stageID object
-func stageIDFromString(stringID string) (*stageID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = stageIDDelimiter
-	lines, err := reader.ReadAll()
-	if err != nil {
-		return nil, fmt.Errorf("Not CSV compatible")
-	}
-
-	if len(lines) != 1 {
-		return nil, fmt.Errorf("1 line per stage")
-	}
-	if len(lines[0]) != 3 {
-		return nil, fmt.Errorf("3 fields allowed")
-	}
-
-	stageResult := &stageID{
-		DatabaseName: lines[0][0],
-		SchemaName:   lines[0][1],
-		StageName:    lines[0][2],
-	}
-	return stageResult, nil
-}
-
 // Stage returns a pointer to the resource representing a stage
 func Stage() *schema.Resource {
 	return &schema.Resource{
@@ -190,10 +136,10 @@ func CreateStage(d *schema.ResourceData, meta interface{}) error {
 		return errors.Wrapf(err, "error creating stage %v", name)
 	}
 
-	stageID := &stageID{
-		DatabaseName: database,
-		SchemaName:   schema,
-		StageName:    name,
+	stageID := &schemaScopedID{
+		Database: database,
+		Schema:   schema,
+		Name:     name,
 	}
 	dataIDInput, err := stageID.String()
 	if err != nil {
@@ -208,14 +154,14 @@ func CreateStage(d *schema.ResourceData, meta interface{}) error {
 // credentials and encryption are omitted, they cannot be read via SHOW or DESCRIBE
 func ReadStage(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	stageID, err := stageIDFromString(d.Id())
+	stageID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := stageID.DatabaseName
-	schema := stageID.SchemaName
-	stage := stageID.StageName
+	dbName := stageID.Database
+	schema := stageID.Schema
+	stage := stageID.Name
 
 	q := snowflake.Stage(stage, dbName, schema).Describe()
 	stageDesc, err := snowflake.DescStage(db, q)
@@ -292,14 +238,14 @@ func ReadStage(d *schema.ResourceData, meta interface{}) error {
 
 // UpdateStage implements schema.UpdateFunc
 func UpdateStage(d *schema.ResourceData, meta interface{}) error {
-	stageID, err := stageIDFromString(d.Id())
+	stageID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := stageID.DatabaseName
-	schema := stageID.SchemaName
-	stage := stageID.StageName
+	dbName := stageID.Database
+	schema := stageID.Schema
+	stage := stageID.Name
 
 	builder := snowflake.Stage(stage, dbName, schema)
 
@@ -370,14 +316,14 @@ func UpdateStage(d *schema.ResourceData, meta interface{}) error {
 // DeleteStage implements schema.DeleteFunc
 func DeleteStage(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	stageID, err := stageIDFromString(d.Id())
+	stageID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := stageID.DatabaseName
-	schema := stageID.SchemaName
-	stage := stageID.StageName
+	dbName := stageID.Database
+	schema := stageID.Schema
+	stage := stageID.Name
 
 	q := snowflake.Stage(stage, dbName, schema).Drop()
 
@@ -394,14 +340,14 @@ func DeleteStage(d *schema.ResourceData, meta interface{}) error {
 // StageExists implements schema.ExistsFunc
 func StageExists(data *schema.ResourceData, meta interface{}) (bool, error) {
 	db := meta.(*sql.DB)
-	stageID, err := stageIDFromString(data.Id())
+	stageID, err := idFromString(data.Id())
 	if err != nil {
 		return false, err
 	}
 
-	dbName := stageID.DatabaseName
-	schema := stageID.SchemaName
-	stage := stageID.StageName
+	dbName := stageID.Database
+	schema := stageID.Schema
+	stage := stageID.Name
 
 	q := snowflake.Stage(stage, dbName, schema).Describe()
 	rows, err := db.Query(q)
diff --git a/pkg/resources/stream.go b/pkg/resources/stream.go
index f6c480ff..ea8af8ff 100644
--- a/pkg/resources/stream.go
+++ b/pkg/resources/stream.go
@@ -1,23 +1,14 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
-	"fmt"
 	"log"
-	"strings"
 
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/pkg/errors"
 )
 
-const (
-	streamIDDelimiter        = '|'
-	streamOnTableIDDelimiter = '.'
-)
-
 var streamSchema = map[string]*schema.Schema{
 	"name": {
 		Type:        schema.TypeString,
@@ -74,84 +65,6 @@ func Stream() *schema.Resource {
 	}
 }
 
-type streamID struct {
-	DatabaseName string
-	SchemaName   string
-	StreamName   string
-}
-
-type streamOnTableID struct {
-	DatabaseName string
-	SchemaName   string
-	OnTableName  string
-}
-
-//String() takes in a streamID object and returns a pipe-delimited string:
-//DatabaseName|SchemaName|StreamName
-func (si *streamID) String() (string, error) {
-	var buf bytes.Buffer
-	csvWriter := csv.NewWriter(&buf)
-	csvWriter.Comma = streamIDDelimiter
-	dataIdentifiers := [][]string{{si.DatabaseName, si.SchemaName, si.StreamName}}
-	err := csvWriter.WriteAll(dataIdentifiers)
-	if err != nil {
-		return "", err
-	}
-	strStreamID := strings.TrimSpace(buf.String())
-	return strStreamID, nil
-}
-
-// streamIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|StreamName
-// and returns a streamID object
-func streamIDFromString(stringID string) (*streamID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = streamIDDelimiter
-	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")
-	}
-
-	streamResult := &streamID{
-		DatabaseName: lines[0][0],
-		SchemaName:   lines[0][1],
-		StreamName:   lines[0][2],
-	}
-	return streamResult, nil
-}
-
-// streamOnTableIDFromString() takes in a dot-delimited string: DatabaseName.SchemaName.TableName
-// and returns a streamOnTableID object
-func streamOnTableIDFromString(stringID string) (*streamOnTableID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = streamOnTableIDDelimiter
-	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("on table format: database_name.schema_name.target_table_name")
-		return nil, fmt.Errorf("invalid format for on_table: %v , expected: <database_name.schema_name.target_table_name>", strings.Join(lines[0], "."))
-	}
-
-	streamOnTableResult := &streamOnTableID{
-		DatabaseName: lines[0][0],
-		SchemaName:   lines[0][1],
-		OnTableName:  lines[0][2],
-	}
-	return streamOnTableResult, nil
-}
-
 // CreateStream implements schema.CreateFunc
 func CreateStream(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
@@ -182,10 +95,10 @@ func CreateStream(d *schema.ResourceData, meta interface{}) error {
 		return errors.Wrapf(err, "error creating stream %v", name)
 	}
 
-	streamID := &streamID{
-		DatabaseName: database,
-		SchemaName:   schema,
-		StreamName:   name,
+	streamID := &schemaScopedID{
+		Database: database,
+		Schema:   schema,
+		Name:     name,
 	}
 	dataIDInput, err := streamID.String()
 	if err != nil {
@@ -199,14 +112,14 @@ func CreateStream(d *schema.ResourceData, meta interface{}) error {
 // ReadStream implements schema.ReadFunc
 func ReadStream(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	streamID, err := streamIDFromString(d.Id())
+	streamID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := streamID.DatabaseName
-	schema := streamID.SchemaName
-	name := streamID.StreamName
+	dbName := streamID.Database
+	schema := streamID.Schema
+	name := streamID.Name
 
 	stmt := snowflake.Stream(name, dbName, schema).Show()
 	row := snowflake.QueryRow(db, stmt)
@@ -237,14 +150,14 @@ func ReadStream(d *schema.ResourceData, meta interface{}) error {
 // DeleteStream implements schema.DeleteFunc
 func DeleteStream(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	streamID, err := streamIDFromString(d.Id())
+	streamID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := streamID.DatabaseName
-	schema := streamID.SchemaName
-	streamName := streamID.StreamName
+	dbName := streamID.Database
+	schema := streamID.Schema
+	streamName := streamID.Name
 
 	q := snowflake.Stream(streamName, dbName, schema).Drop()
 
@@ -260,14 +173,14 @@ func DeleteStream(d *schema.ResourceData, meta interface{}) error {
 
 // UpdateStream implements schema.UpdateFunc
 func UpdateStream(d *schema.ResourceData, meta interface{}) error {
-	streamID, err := streamIDFromString(d.Id())
+	streamID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := streamID.DatabaseName
-	schema := streamID.SchemaName
-	streamName := streamID.StreamName
+	dbName := streamID.Database
+	schema := streamID.Schema
+	streamName := streamID.Name
 
 	builder := snowflake.Stream(streamName, dbName, schema)
 
diff --git a/pkg/resources/table.go b/pkg/resources/table.go
index f8e56943..86c99bf8 100644
--- a/pkg/resources/table.go
+++ b/pkg/resources/table.go
@@ -1,22 +1,14 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
-	"fmt"
 	"log"
-	"strings"
 
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/pkg/errors"
 )
 
-const (
-	tableIDDelimiter = '|'
-)
-
 var tableSchema = map[string]*schema.Schema{
 	"name": {
 		Type:        schema.TypeString,
@@ -83,52 +75,6 @@ func Table() *schema.Resource {
 	}
 }
 
-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(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
@@ -159,10 +105,10 @@ func CreateTable(d *schema.ResourceData, meta interface{}) error {
 		return errors.Wrapf(err, "error creating table %v", name)
 	}
 
-	tableID := &tableID{
-		DatabaseName: database,
-		SchemaName:   schema,
-		TableName:    name,
+	tableID := &schemaScopedID{
+		Database: database,
+		Schema:   schema,
+		Name:     name,
 	}
 	dataIDInput, err := tableID.String()
 	if err != nil {
@@ -176,11 +122,11 @@ func CreateTable(d *schema.ResourceData, meta interface{}) error {
 // ReadTable implements schema.ReadFunc
 func ReadTable(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	tableID, err := tableIDFromString(d.Id())
+	tableID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
-	builder := snowflake.Table(tableID.TableName, tableID.DatabaseName, tableID.SchemaName)
+	builder := snowflake.Table(tableID.Name, tableID.Database, tableID.Schema)
 
 	row := snowflake.QueryRow(db, builder.Show())
 	table, err := snowflake.ScanTable(row)
@@ -209,8 +155,8 @@ func ReadTable(d *schema.ResourceData, meta interface{}) error {
 	toSet := map[string]interface{}{
 		"name":     table.TableName.String,
 		"owner":    table.Owner.String,
-		"database": tableID.DatabaseName,
-		"schema":   tableID.SchemaName,
+		"database": tableID.Database,
+		"schema":   tableID.Schema,
 		"comment":  table.Comment.String,
 		"column":   snowflake.NewColumns(tableDescription).Flatten(),
 	}
@@ -226,14 +172,14 @@ func ReadTable(d *schema.ResourceData, meta interface{}) error {
 
 // UpdateTable implements schema.UpdateFunc
 func UpdateTable(d *schema.ResourceData, meta interface{}) error {
-	tableID, err := tableIDFromString(d.Id())
+	tableID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := tableID.DatabaseName
-	schema := tableID.SchemaName
-	tableName := tableID.TableName
+	dbName := tableID.Database
+	schema := tableID.Schema
+	tableName := tableID.Name
 
 	builder := snowflake.Table(tableName, dbName, schema)
 
@@ -253,14 +199,14 @@ func UpdateTable(d *schema.ResourceData, meta interface{}) error {
 // DeleteTable implements schema.DeleteFunc
 func DeleteTable(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	tableID, err := tableIDFromString(d.Id())
+	tableID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	dbName := tableID.DatabaseName
-	schema := tableID.SchemaName
-	tableName := tableID.TableName
+	dbName := tableID.Database
+	schema := tableID.Schema
+	tableName := tableID.Name
 
 	q := snowflake.Table(tableName, dbName, schema).Drop()
 
@@ -277,14 +223,14 @@ func DeleteTable(d *schema.ResourceData, meta interface{}) error {
 // TableExists implements schema.ExistsFunc
 func TableExists(data *schema.ResourceData, meta interface{}) (bool, error) {
 	db := meta.(*sql.DB)
-	tableID, err := tableIDFromString(data.Id())
+	tableID, err := idFromString(data.Id())
 	if err != nil {
 		return false, err
 	}
 
-	dbName := tableID.DatabaseName
-	schema := tableID.SchemaName
-	tableName := tableID.TableName
+	dbName := tableID.Database
+	schema := tableID.Schema
+	tableName := tableID.Name
 
 	q := snowflake.Table(tableName, dbName, schema).Show()
 	rows, err := db.Query(q)
diff --git a/pkg/resources/task.go b/pkg/resources/task.go
index 437c6cc8..589c4dd8 100644
--- a/pkg/resources/task.go
+++ b/pkg/resources/task.go
@@ -1,12 +1,9 @@
 package resources
 
 import (
-	"bytes"
 	"database/sql"
-	"encoding/csv"
 	"fmt"
 	"log"
-	"strings"
 
 	"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -14,10 +11,6 @@ import (
 	"github.com/pkg/errors"
 )
 
-const (
-	taskIDDelimiter = '|'
-)
-
 var taskSchema = map[string]*schema.Schema{
 	"enabled": {
 		Type:        schema.TypeBool,
@@ -89,27 +82,6 @@ var taskSchema = map[string]*schema.Schema{
 	},
 }
 
-type taskID struct {
-	DatabaseName string
-	SchemaName   string
-	TaskName     string
-}
-
-//String() takes in a taskID object and returns a pipe-delimited string:
-//DatabaseName|SchemaName|TaskName
-func (t *taskID) String() (string, error) {
-	var buf bytes.Buffer
-	csvWriter := csv.NewWriter(&buf)
-	csvWriter.Comma = taskIDDelimiter
-	dataIdentifiers := [][]string{{t.DatabaseName, t.SchemaName, t.TaskName}}
-	err := csvWriter.WriteAll(dataIdentifiers)
-	if err != nil {
-		return "", err
-	}
-	strTaskID := strings.TrimSpace(buf.String())
-	return strTaskID, nil
-}
-
 // difference find keys in a but not in b
 func difference(a, b map[string]interface{}) map[string]interface{} {
 	diff := make(map[string]interface{})
@@ -202,31 +174,6 @@ func resumeTask(root *snowflake.TaskBuilder, meta interface{}) {
 	}
 }
 
-// taskIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|TaskName
-// and returns a taskID object
-func taskIDFromString(stringID string) (*taskID, error) {
-	reader := csv.NewReader(strings.NewReader(stringID))
-	reader.Comma = pipeIDDelimiter
-	lines, err := reader.ReadAll()
-	if err != nil {
-		return nil, fmt.Errorf("Not CSV compatible")
-	}
-
-	if len(lines) != 1 {
-		return nil, fmt.Errorf("1 line per task")
-	}
-	if len(lines[0]) != 3 {
-		return nil, fmt.Errorf("3 fields allowed")
-	}
-
-	taskResult := &taskID{
-		DatabaseName: lines[0][0],
-		SchemaName:   lines[0][1],
-		TaskName:     lines[0][2],
-	}
-	return taskResult, nil
-}
-
 // Task returns a pointer to the resource representing a task
 func Task() *schema.Resource {
 	return &schema.Resource{
@@ -245,14 +192,14 @@ func Task() *schema.Resource {
 // ReadTask implements schema.ReadFunc
 func ReadTask(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	taskID, err := taskIDFromString(d.Id())
+	taskID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	database := taskID.DatabaseName
-	schema := taskID.SchemaName
-	name := taskID.TaskName
+	database := taskID.Database
+	schema := taskID.Schema
+	name := taskID.Name
 
 	builder := snowflake.Task(name, database, schema)
 	q := builder.Show()
@@ -411,12 +358,12 @@ func CreateTask(d *schema.ResourceData, meta interface{}) error {
 		}
 	}
 
-	taskID := &taskID{
-		DatabaseName: database,
-		SchemaName:   dbSchema,
-		TaskName:     name,
+	id := &schemaScopedID{
+		Database: database,
+		Schema:   dbSchema,
+		Name:     name,
 	}
-	dataIDInput, err := taskID.String()
+	dataIDInput, err := id.String()
 	if err != nil {
 		return err
 	}
@@ -427,15 +374,15 @@ func CreateTask(d *schema.ResourceData, meta interface{}) error {
 
 // UpdateTask implements schema.UpdateFunc
 func UpdateTask(d *schema.ResourceData, meta interface{}) error {
-	taskID, err := taskIDFromString(d.Id())
+	taskID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
 	db := meta.(*sql.DB)
-	database := taskID.DatabaseName
-	dbSchema := taskID.SchemaName
-	name := taskID.TaskName
+	database := taskID.Database
+	dbSchema := taskID.Schema
+	name := taskID.Name
 	builder := snowflake.Task(name, database, dbSchema)
 
 	root, err := getActiveRootTaskAndSuspend(d, meta)
@@ -609,14 +556,14 @@ func UpdateTask(d *schema.ResourceData, meta interface{}) error {
 // DeleteTask implements schema.DeleteFunc
 func DeleteTask(d *schema.ResourceData, meta interface{}) error {
 	db := meta.(*sql.DB)
-	taskID, err := taskIDFromString(d.Id())
+	taskID, err := idFromString(d.Id())
 	if err != nil {
 		return err
 	}
 
-	database := taskID.DatabaseName
-	schema := taskID.SchemaName
-	name := taskID.TaskName
+	database := taskID.Database
+	schema := taskID.Schema
+	name := taskID.Name
 
 	root, err := getActiveRootTaskAndSuspend(d, meta)
 	if err != nil {
@@ -642,14 +589,14 @@ func DeleteTask(d *schema.ResourceData, meta interface{}) error {
 // TaskExists implements schema.ExistsFunc
 func TaskExists(data *schema.ResourceData, meta interface{}) (bool, error) {
 	db := meta.(*sql.DB)
-	taskID, err := taskIDFromString(data.Id())
+	taskID, err := idFromString(data.Id())
 	if err != nil {
 		return false, err
 	}
 
-	database := taskID.DatabaseName
-	schema := taskID.SchemaName
-	name := taskID.TaskName
+	database := taskID.Database
+	schema := taskID.Schema
+	name := taskID.Name
 
 	q := snowflake.Task(name, database, schema).Show()
 	rows, err := db.Query(q)


Test patch:
diff --git a/pkg/resources/grant_helpers_internal_test.go b/pkg/resources/grant_helpers_internal_test.go
index aa4b0e77..c15e5e6c 100644
--- a/pkg/resources/grant_helpers_internal_test.go
+++ b/pkg/resources/grant_helpers_internal_test.go
@@ -9,20 +9,9 @@ import (
 
 func TestGrantIDFromString(t *testing.T) {
 	r := require.New(t)
-	// Vanilla without GrantOption
-	id := "database_name|schema|view_name|privilege"
-	grant, err := grantIDFromString(id)
-	r.NoError(err)
-
-	r.Equal("database_name", grant.ResourceName)
-	r.Equal("schema", grant.SchemaName)
-	r.Equal("view_name", grant.ObjectName)
-	r.Equal("privilege", grant.Privilege)
-	r.Equal(false, grant.GrantOption)
-
 	// Vanilla with GrantOption
-	id = "database_name|schema|view_name|privilege|true"
-	grant, err = grantIDFromString(id)
+	id := "database_name|schema|view_name|privilege|true"
+	grant, err := grantIDFromString(id)
 	r.NoError(err)
 
 	r.Equal("database_name", grant.ResourceName)
@@ -44,28 +33,22 @@ func TestGrantIDFromString(t *testing.T) {
 	// Bad ID -- not enough fields
 	id = "database|name-privilege"
 	_, err = grantIDFromString(id)
-	r.Equal(fmt.Errorf("4 or 5 fields allowed"), err)
+	r.Equal(fmt.Errorf("wrong number of fields in record"), err)
 
 	// Bad ID -- privilege in wrong area
 	id = "database||name-privilege"
 	_, err = grantIDFromString(id)
-	r.Equal(fmt.Errorf("4 or 5 fields allowed"), err)
+	r.Equal(fmt.Errorf("wrong number of fields in record"), err)
 
 	// too many fields
 	id = "database_name|schema|view_name|privilege|false|2"
 	_, err = grantIDFromString(id)
-	r.Equal(fmt.Errorf("4 or 5 fields allowed"), err)
+	r.Equal(fmt.Errorf("wrong number of fields in record"), err)
 
 	// 0 lines
 	id = ""
 	_, err = grantIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per grant"), err)
-
-	// 2 lines
-	id = `database_name|schema|view_name|privilege
-	database_name|schema|view_name|privilege`
-	_, err = grantIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per grant"), err)
+	r.Equal(fmt.Errorf("EOF"), err)
 }
 
 func TestGrantStruct(t *testing.T) {
diff --git a/pkg/resources/ids_test.go b/pkg/resources/ids_test.go
new file mode 100644
index 00000000..10eba503
--- /dev/null
+++ b/pkg/resources/ids_test.go
@@ -0,0 +1,61 @@
+package resources
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIDFromString(t *testing.T) {
+	r := require.New(t)
+	// Vanilla
+	id, err := idFromString("database_name|schema_name|pipe")
+	r.NoError(err)
+	r.Equal("database_name", id.Database)
+	r.Equal("schema_name", id.Schema)
+	r.Equal("pipe", id.Name)
+
+	_, err = idFromString("database")
+	r.Equal(fmt.Errorf("wrong number of fields in record"), err)
+
+	// Bad ID
+	_, err = idFromString("||")
+	r.NoError(err)
+
+	// 0 lines
+	_, err = idFromString("")
+	r.Equal(fmt.Errorf("EOF"), err)
+}
+
+func TestIDStruct(t *testing.T) {
+	r := require.New(t)
+
+	// Vanilla
+	id := &schemaScopedID{
+		Database: "database_name",
+		Schema:   "schema_name",
+		Name:     "pipe",
+	}
+	sID, err := id.String()
+	r.NoError(err)
+	r.Equal("database_name|schema_name|pipe", sID)
+
+	// Empty grant
+	id = &schemaScopedID{}
+	sID, err = id.String()
+	r.NoError(err)
+	r.Equal("||", sID)
+
+	// Grant with extra delimiters
+	id = &schemaScopedID{
+		Database: "database|name",
+		Name:     "pipe|name",
+	}
+	sID, err = id.String()
+	r.NoError(err)
+	newID, err := idFromString(sID)
+	r.NoError(err)
+	r.Equal("database|name", newID.Database)
+	r.Equal("pipe|name", newID.Name)
+}
diff --git a/pkg/resources/pipe_internal_test.go b/pkg/resources/pipe_internal_test.go
index 62f3a93e..7df69c9c 100644
--- a/pkg/resources/pipe_internal_test.go
+++ b/pkg/resources/pipe_internal_test.go
@@ -7,70 +7,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestPipeIDFromString(t *testing.T) {
-	r := require.New(t)
-	// Vanilla
-	id := "database_name|schema_name|pipe"
-	pipe, err := pipeIDFromString(id)
-	r.NoError(err)
-	r.Equal("database_name", pipe.DatabaseName)
-	r.Equal("schema_name", pipe.SchemaName)
-	r.Equal("pipe", pipe.PipeName)
-
-	// Bad ID -- not enough fields
-	id = "database"
-	_, err = pipeIDFromString(id)
-	r.Equal(fmt.Errorf("3 fields allowed"), err)
-
-	// Bad ID
-	id = "||"
-	_, err = pipeIDFromString(id)
-	r.NoError(err)
-
-	// 0 lines
-	id = ""
-	_, err = pipeIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per pipe"), err)
-
-	// 2 lines
-	id = `database_name|schema_name|pipe
-	database_name|schema_name|pipe`
-	_, err = pipeIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per pipe"), err)
-}
-
-func TestPipeStruct(t *testing.T) {
-	r := require.New(t)
-
-	// Vanilla
-	pipe := &pipeID{
-		DatabaseName: "database_name",
-		SchemaName:   "schema_name",
-		PipeName:     "pipe",
-	}
-	sID, err := pipe.String()
-	r.NoError(err)
-	r.Equal("database_name|schema_name|pipe", sID)
-
-	// Empty grant
-	pipe = &pipeID{}
-	sID, err = pipe.String()
-	r.NoError(err)
-	r.Equal("||", sID)
-
-	// Grant with extra delimiters
-	pipe = &pipeID{
-		DatabaseName: "database|name",
-		PipeName:     "pipe|name",
-	}
-	sID, err = pipe.String()
-	r.NoError(err)
-	newPipe, err := pipeIDFromString(sID)
-	r.NoError(err)
-	r.Equal("database|name", newPipe.DatabaseName)
-	r.Equal("pipe|name", newPipe.PipeName)
-}
-
 const pipeCopyStatementTemplate string = "COPY INTO MY_DATABASE.MY_SCHEMA.%[4]s (%[1]s%[2]sID%[1]s%[2]s,VALUE%[1]s) FROM (%[1]s%[2]sSELECT%[1]s%[2]s%[2]sSRC.$1%[1]s%[2]s%[2]s,SRC.$2%[1]s%[2]sFROM @MY_DATABASE.MY_SCHEMA.MY_STAGE AS SRC%[1]s)%[1]sFILE_FORMAT = (%[1]s%[2]sFORMAT_NAME = MY_DATABASE.MY_SCHEMA.JSON%[1]s)%[1]sON_ERROR = CONTINUE%[3]s"
 
 func generatecopyStatement(lineEnding string, indent string, includeSemiColon bool, tableName string) string {
diff --git a/pkg/resources/schema_internal_test.go b/pkg/resources/schema_internal_test.go
deleted file mode 100644
index 9e4e8d70..00000000
--- a/pkg/resources/schema_internal_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package resources
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestSchemaIDFromString(t *testing.T) {
-	r := require.New(t)
-	// Vanilla
-	id := "database_name|schema"
-	schema, err := schemaIDFromString(id)
-	r.NoError(err)
-	r.Equal("database_name", schema.DatabaseName)
-	r.Equal("schema", schema.SchemaName)
-
-	// Bad ID -- not enough fields
-	id = "database"
-	_, err = schemaIDFromString(id)
-	r.Equal(fmt.Errorf("2 fields allowed"), err)
-
-	// Bad ID
-	id = "|"
-	_, err = schemaIDFromString(id)
-	r.NoError(err)
-
-	// 0 lines
-	id = ""
-	_, err = schemaIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per schema"), err)
-
-	// 2 lines
-	id = `database_name|schema
-	database_name|schema`
-	_, err = schemaIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per schema"), err)
-}
-
-func TestSchemaStruct(t *testing.T) {
-	r := require.New(t)
-
-	// Vanilla
-	schema := &schemaID{
-		DatabaseName: "database_name",
-		SchemaName:   "schema",
-	}
-	sID, err := schema.String()
-	r.NoError(err)
-	r.Equal("database_name|schema", sID)
-
-	// Empty grant
-	schema = &schemaID{}
-	sID, err = schema.String()
-	r.NoError(err)
-	r.Equal("|", sID)
-
-	// Grant with extra delimiters
-	schema = &schemaID{
-		DatabaseName: "database|name",
-		SchemaName:   "schema|name",
-	}
-	sID, err = schema.String()
-	r.NoError(err)
-	newSchema, err := schemaIDFromString(sID)
-	r.NoError(err)
-	r.Equal("database|name", newSchema.DatabaseName)
-	r.Equal("schema|name", newSchema.SchemaName)
-}
diff --git a/pkg/resources/stream_internal_test.go b/pkg/resources/stream_internal_test.go
index 5d76a56d..4f699bfc 100644
--- a/pkg/resources/stream_internal_test.go
+++ b/pkg/resources/stream_internal_test.go
@@ -7,70 +7,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestStreamIDFromString(t *testing.T) {
-	r := require.New(t)
-	// Vanilla
-	id := "database_name|schema_name|stream"
-	stream, err := streamIDFromString(id)
-	r.NoError(err)
-	r.Equal("database_name", stream.DatabaseName)
-	r.Equal("schema_name", stream.SchemaName)
-	r.Equal("stream", stream.StreamName)
-
-	// Bad ID -- not enough fields
-	id = "database"
-	_, err = streamIDFromString(id)
-	r.Equal(fmt.Errorf("3 fields allowed"), err)
-
-	// Bad ID
-	id = "||"
-	_, err = streamIDFromString(id)
-	r.NoError(err)
-
-	// 0 lines
-	id = ""
-	_, err = streamIDFromString(id)
-	r.Equal(fmt.Errorf("1 line at a time"), err)
-
-	// 2 lines
-	id = `database_name|schema_name|stream
-	database_name|schema_name|stream`
-	_, err = streamIDFromString(id)
-	r.Equal(fmt.Errorf("1 line at a time"), err)
-}
-
-func TestStreamStruct(t *testing.T) {
-	r := require.New(t)
-
-	// Vanilla
-	stream := &streamID{
-		DatabaseName: "database_name",
-		SchemaName:   "schema_name",
-		StreamName:   "stream_name",
-	}
-	sID, err := stream.String()
-	r.NoError(err)
-	r.Equal("database_name|schema_name|stream_name", sID)
-
-	// Empty grant
-	stream = &streamID{}
-	sID, err = stream.String()
-	r.NoError(err)
-	r.Equal("||", sID)
-
-	// Grant with extra delimiters
-	stream = &streamID{
-		DatabaseName: "database|name",
-		StreamName:   "stream|name",
-	}
-	sID, err = stream.String()
-	r.NoError(err)
-	newStream, err := streamIDFromString(sID)
-	r.NoError(err)
-	r.Equal("database|name", newStream.DatabaseName)
-	r.Equal("stream|name", newStream.StreamName)
-}
-
 func TestStreamOnTableIDFromString(t *testing.T) {
 	r := require.New(t)
 	// Vanilla
@@ -94,11 +30,5 @@ func TestStreamOnTableIDFromString(t *testing.T) {
 	// 0 lines
 	id = ""
 	_, err = streamOnTableIDFromString(id)
-	r.Equal(fmt.Errorf("1 line at a time"), err)
-
-	// 2 lines
-	id = `database_name.schema_name.target_table_name
-	database_name.schema_name.target_table_name`
-	_, err = streamOnTableIDFromString(id)
-	r.Equal(fmt.Errorf("1 line at a time"), err)
+	r.Equal(fmt.Errorf("expecting 1 line"), err)
 }
diff --git a/pkg/resources/table_internal_test.go b/pkg/resources/table_internal_test.go
deleted file mode 100644
index f35c9530..00000000
--- a/pkg/resources/table_internal_test.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package resources
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestTableIDFromString(t *testing.T) {
-	r := require.New(t)
-	// Vanilla
-	id := "database_name|schema_name|table"
-	table, err := tableIDFromString(id)
-	r.NoError(err)
-	r.Equal("database_name", table.DatabaseName)
-	r.Equal("schema_name", table.SchemaName)
-	r.Equal("table", table.TableName)
-
-	// Bad ID -- not enough fields
-	id = "database"
-	_, err = tableIDFromString(id)
-	r.Equal(fmt.Errorf("3 fields allowed"), err)
-
-	// Bad ID
-	id = "||"
-	_, err = tableIDFromString(id)
-	r.NoError(err)
-
-	// 0 lines
-	id = ""
-	_, err = tableIDFromString(id)
-	r.Equal(fmt.Errorf("1 line at a time"), err)
-
-	// 2 lines
-	id = `database_name|schema_name|table
-	database_name|schema_name|table`
-	_, err = tableIDFromString(id)
-	r.Equal(fmt.Errorf("1 line at a time"), err)
-}
-
-func TestTableStruct(t *testing.T) {
-	r := require.New(t)
-
-	// Vanilla
-	table := &tableID{
-		DatabaseName: "database_name",
-		SchemaName:   "schema_name",
-		TableName:    "table",
-	}
-	sID, err := table.String()
-	r.NoError(err)
-	r.Equal("database_name|schema_name|table", sID)
-
-	// Empty grant
-	table = &tableID{}
-	sID, err = table.String()
-	r.NoError(err)
-	r.Equal("||", sID)
-
-	// Grant with extra delimiters
-	table = &tableID{
-		DatabaseName: "database|name",
-		TableName:    "table|name",
-	}
-	sID, err = table.String()
-	r.NoError(err)
-	newTable, err := tableIDFromString(sID)
-	r.NoError(err)
-	r.Equal("database|name", newTable.DatabaseName)
-	r.Equal("table|name", newTable.TableName)
-}
diff --git a/pkg/resources/task_internal_test.go b/pkg/resources/task_internal_test.go
deleted file mode 100644
index 9d6319e6..00000000
--- a/pkg/resources/task_internal_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package resources
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestStringFromTaskID(t *testing.T) {
-	r := require.New(t)
-	task := taskID{DatabaseName: "test_db", SchemaName: "test_schema", TaskName: "test_task"}
-	id, err := task.String()
-	r.NoError(err)
-	r.Equal(id, "test_db|test_schema|test_task")
-}
-
-func TestTaskIDFromString(t *testing.T) {
-	r := require.New(t)
-
-	id := "test_db|test_schema|test_task"
-	task, err := taskIDFromString(id)
-	r.NoError(err)
-	r.Equal("test_db", task.DatabaseName)
-	r.Equal("test_schema", task.SchemaName)
-	r.Equal("test_task", task.TaskName)
-
-	id = "test_db"
-	_, err = taskIDFromString(id)
-	r.Equal(fmt.Errorf("3 fields allowed"), err)
-
-	// Bad ID
-	id = "|"
-	_, err = taskIDFromString(id)
-	r.Equal(fmt.Errorf("3 fields allowed"), err)
-
-	// 0 lines
-	id = ""
-	_, err = taskIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per task"), err)
-
-	// 2 lines
-	id = `database|schema|task
-		database|schema|task`
-	_, err = taskIDFromString(id)
-	r.Equal(fmt.Errorf("1 line per task"), err)
-
-}