Skip to content

Commit

Permalink
feat: Add replication support (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
anton-chekanov authored Feb 3, 2022
1 parent 7ac4d54 commit f519cfc
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/resources/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ resource "snowflake_database" "test2" {
- **comment** (String)
- **data_retention_time_in_days** (Number)
- **from_database** (String) Specify a database to create a clone from.
- **from_replica** (String) Specify a fully-qualified path to a database to create a replica from.
- **from_share** (Map of String) Specify a provider and a share in this map to create a database from a share.
- **id** (String) The ID of this resource.
- **tag** (Block List) Definitions of a tag to associate with the resource. (see [below for nested schema](#nestedblock--tag))
Expand Down
36 changes: 31 additions & 5 deletions pkg/resources/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,21 @@ var databaseSchema = map[string]*schema.Schema{
Description: "Specify a provider and a share in this map to create a database from a share.",
Optional: true,
ForceNew: true,
ConflictsWith: []string{"from_database"},
ConflictsWith: []string{"from_database", "from_replica"},
},
"from_database": {
Type: schema.TypeString,
Description: "Specify a database to create a clone from.",
Optional: true,
ForceNew: true,
ConflictsWith: []string{"from_share"},
ConflictsWith: []string{"from_share", "from_replica"},
},
"from_replica": {
Type: schema.TypeString,
Description: "Specify a fully-qualified path to a database to create a replica from.",
Optional: true,
ForceNew: true,
ConflictsWith: []string{"from_share", "from_database"},
},
"tag": tagReferenceSchema,
}
Expand Down Expand Up @@ -71,6 +78,10 @@ func CreateDatabase(d *schema.ResourceData, meta interface{}) error {
return createDatabaseFromDatabase(d, meta)
}

if _, ok := d.GetOk("from_replica"); ok {
return createDatabaseFromReplica(d, meta)
}

return CreateResource("database", databaseProperties, databaseSchema, snowflake.Database, ReadDatabase)(d, meta)
}

Expand Down Expand Up @@ -114,6 +125,23 @@ func createDatabaseFromDatabase(d *schema.ResourceData, meta interface{}) error
return ReadDatabase(d, meta)
}

func createDatabaseFromReplica(d *schema.ResourceData, meta interface{}) error {
sourceDb := d.Get("from_replica").(string)

db := meta.(*sql.DB)
name := d.Get("name").(string)
builder := snowflake.DatabaseFromReplica(name, sourceDb)

err := snowflake.Exec(db, builder.Create())
if err != nil {
return errors.Wrapf(err, "error creating a secondary database %v from database %v", name, sourceDb)
}

d.SetId(name)

return ReadDatabase(d, meta)
}

func ReadDatabase(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := d.Id()
Expand Down Expand Up @@ -147,12 +175,10 @@ func ReadDatabase(d *schema.ResourceData, meta interface{}) error {
return err
}

err = d.Set("data_retention_time_in_days", i)
return err
return d.Set("data_retention_time_in_days", i)
}

func UpdateDatabase(d *schema.ResourceData, meta interface{}) error {
log.Printf("[DEBUG] updating database %v", d.Id())
return UpdateResource("database", databaseProperties, databaseSchema, snowflake.Database, ReadDatabase)(d, meta)
}

Expand Down
26 changes: 23 additions & 3 deletions pkg/resources/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ func TestDatabaseCreate(t *testing.T) {
}

func expectRead(mock sqlmock.Sqlmock) {
rows := sqlmock.NewRows([]string{"created_on", "name", "is_default", "is_current", "origin", "owner", "comment", "options", "retention_time"}).AddRow("created_on", "good_name", "is_default", "is_current", "origin", "owner", "mock comment", "options", "1")
mock.ExpectQuery("SHOW DATABASES LIKE 'good_name'").WillReturnRows(rows)
dbRows := sqlmock.NewRows([]string{"created_on", "name", "is_default", "is_current", "origin", "owner", "comment", "options", "retention_time"}).AddRow("created_on", "good_name", "is_default", "is_current", "origin", "owner", "mock comment", "options", "1")
mock.ExpectQuery("SHOW DATABASES LIKE 'good_name'").WillReturnRows(dbRows)
}

func TestDatabaseRead(t *testing.T) {
r := require.New(t)

d := database(t, "good_name", map[string]interface{}{"name": "good_name"})
d := database(t, "good_name", map[string]interface{}{
"name": "good_name",
})

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
expectRead(mock)
Expand Down Expand Up @@ -106,3 +108,21 @@ func TestDatabaseCreateFromDatabase(t *testing.T) {
r.NoError(err)
})
}

func TestDatabaseCreateFromReplica(t *testing.T) {
r := require.New(t)

in := map[string]interface{}{
"name": "good_name",
"from_replica": "abc123",
}
d := schema.TestResourceDataRaw(t, resources.Database().Schema, in)
r.NotNil(d)

WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) {
mock.ExpectExec(`CREATE DATABASE "good_name" AS REPLICA OF "abc123"`).WillReturnResult(sqlmock.NewResult(1, 1))
expectRead(mock)
err := resources.CreateDatabase(d, db)
r.NoError(err)
})
}
19 changes: 19 additions & 0 deletions pkg/snowflake/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ func (dsb *DatabaseCloneBuilder) Create() string {
return fmt.Sprintf(`CREATE DATABASE "%v" CLONE "%v"`, dsb.name, dsb.database)
}

// DatabaseReplicaBuilder is a basic builder that just creates databases from an avilable replication source
type DatabaseReplicaBuilder struct {
name string
replica string
}

// DatabaseFromReplica returns a pointer to a builder that can create a database from an avilable replication source
func DatabaseFromReplica(name, replica string) *DatabaseReplicaBuilder {
return &DatabaseReplicaBuilder{
name: name,
replica: replica,
}
}

// Create returns the SQL statement required to create a database from an avilable replication source
func (dsb *DatabaseReplicaBuilder) Create() string {
return fmt.Sprintf(`CREATE DATABASE "%v" AS REPLICA OF "%v"`, dsb.name, dsb.replica)
}

type database struct {
CreatedOn sql.NullString `db:"created_on"`
DBName sql.NullString `db:"name"`
Expand Down
7 changes: 7 additions & 0 deletions pkg/snowflake/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ func TestDatabaseCreateFromDatabase(t *testing.T) {
r.Equal(`CREATE DATABASE "db1" CLONE "abc123"`, q)
}

func TestDatabaseCreateFromReplica(t *testing.T) {
r := require.New(t)
db := snowflake.DatabaseFromReplica("db1", "abc123")
q := db.Create()
r.Equal(`CREATE DATABASE "db1" AS REPLICA OF "abc123"`, q)
}

func TestListDatabases(t *testing.T) {
r := require.New(t)
mockDB, mock, err := sqlmock.New()
Expand Down
1 change: 1 addition & 0 deletions pkg/snowflake/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
ResourceMonitorType EntityType = "RESOURCE MONITOR"
RoleType EntityType = "ROLE"
ShareType EntityType = "SHARE"
ReplicationType EntityType = "REPLICATION"
StorageIntegrationType EntityType = "STORAGE INTEGRATION"
NotificationIntegrationType EntityType = "NOTIFICATION INTEGRATION"
SecurityIntegrationType EntityType = "SECURITY INTEGRATION"
Expand Down
56 changes: 56 additions & 0 deletions pkg/snowflake/replication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package snowflake

import (
"database/sql"
"fmt"

"github.com/jmoiron/sqlx"
)

// Replication returns a pointer to a Builder that abstracts the DDL operations for a replication.
//
// Supported DDL operations are:
// - SHOW REPLICATION DATABASES
//
// [Snowflake Reference](https://docs.snowflake.com/en/user-guide/database-replication-config.html)

// ReplicationBuilder is a basic builder that enables replication on databases
type ReplicationBuilder struct {
database string
}

// DatabaseFromDatabase returns a pointer to a builder that can create a database from a source database
func Replication(database string) *ReplicationBuilder {
return &ReplicationBuilder{
database: database,
}
}

type replication struct {
Region sql.NullString `db:"snowflake_region"`
CreatedOn sql.NullString `db:"created_on"`
AccountName sql.NullString `db:"account_name"`
DBName sql.NullString `db:"name"`
Comment sql.NullString `db:"comment"`
IsPrimary sql.NullBool `db:"is_primary"`
Primary sql.NullString `db:"primary"`
ReplAccounts sql.NullString `db:"replication_allowed_to_accounts"`
FailoverAccounts sql.NullString `db:"failover_allowed_to_accounts"`
Org sql.NullString `db:"organization_name"`
AccountLocator sql.NullString `db:"account_locator"`
}

func ScanReplication(rows *sqlx.Rows, AccName string) (*replication, error) {
for rows.Next() {
r := &replication{}
err := rows.StructScan(r)
if r.AccountName.String == AccName {
return r, err
}
}
return nil, sql.ErrNoRows
}

func (rb *ReplicationBuilder) Show() string {
return fmt.Sprintf(`SHOW REPLICATION DATABASES LIKE '%s'`, rb.database)
}

0 comments on commit f519cfc

Please sign in to comment.