github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/cassandra/schema.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package cassandra
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  
    27  	"bytes"
    28  
    29  	"github.com/gocql/gocql"
    30  	"github.com/pkg/errors"
    31  	"github.com/uber-go/dosa"
    32  )
    33  
    34  // CreateScope creates a keyspace
    35  func (c *Connector) CreateScope(ctx context.Context, scope string) error {
    36  	// drop the old scope, ignoring errors
    37  	_ = c.DropScope(ctx, scope)
    38  
    39  	ksn := CleanupKeyspaceName(scope)
    40  	// TODO: improve the replication factor, should have 3 replicas in each datacenter
    41  	err := c.Session.Query(
    42  		fmt.Sprintf(`CREATE KEYSPACE "%s" WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 1}`,
    43  			ksn)).
    44  		Exec()
    45  	if err != nil {
    46  		return errors.Wrapf(err, "Unable to create keyspace %q", ksn)
    47  	}
    48  	return nil
    49  }
    50  
    51  // TruncateScope is not implemented
    52  func (c *Connector) TruncateScope(ctx context.Context, scope string) error {
    53  	panic("not implemented")
    54  }
    55  
    56  // DropScope is not implemented
    57  func (c *Connector) DropScope(ctx context.Context, scope string) error {
    58  	ksn := CleanupKeyspaceName(scope)
    59  	err := c.Session.Query(fmt.Sprintf(`DROP KEYSPACE IF EXISTS "%s"`, ksn)).Exec()
    60  	if err != nil {
    61  		return errors.Wrapf(err, "Unable to drop keyspace %q", ksn)
    62  	}
    63  	return nil
    64  }
    65  
    66  // RepairableSchemaMismatchError is an error describing what can be added to make
    67  // this schema current. It might include a lot of tables or columns
    68  type RepairableSchemaMismatchError struct {
    69  	MissingColumns []MissingColumn
    70  	MissingTables  []string
    71  }
    72  
    73  // MissingColumn describes a column that is missing
    74  type MissingColumn struct {
    75  	Column    dosa.ColumnDefinition
    76  	Tablename string
    77  }
    78  
    79  // HasMissing returns true if there are missing columns
    80  func (m *RepairableSchemaMismatchError) HasMissing() bool {
    81  	return m.MissingColumns != nil || m.MissingTables != nil
    82  }
    83  
    84  // Error prints a human-readable error message describing the first missing table or column
    85  func (m *RepairableSchemaMismatchError) Error() string {
    86  	if m.MissingTables != nil {
    87  		return fmt.Sprintf("Missing %d tables (first is %q)", len(m.MissingTables), m.MissingTables[0])
    88  	}
    89  	return fmt.Sprintf("Missing %d columns (first is %q in table %q)", len(m.MissingColumns), m.MissingColumns[0].Column.Name, m.MissingColumns[0].Tablename)
    90  }
    91  
    92  // Check partition keys
    93  func checkPartitionKeys(ed *dosa.EntityDefinition, md *gocql.TableMetadata) error {
    94  	if len(ed.Key.PartitionKeys) != len(md.PartitionKey) {
    95  		return fmt.Errorf("Table %q partition key length mismatch (was %d should be %d)", ed.Name, len(md.PartitionKey), len(ed.Key.PartitionKeys))
    96  	}
    97  	for i, pk := range ed.Key.PartitionKeys {
    98  		if md.PartitionKey[i].Name != pk {
    99  			return fmt.Errorf("Table %q partition key mismatch (should be %q)", ed.Name, ed.Key.PartitionKeys)
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  func checkClusteringKeys(ed *dosa.EntityDefinition, md *gocql.TableMetadata) error {
   106  	if len(ed.Key.ClusteringKeys) != len(md.ClusteringColumns) {
   107  		return fmt.Errorf("Table %q clustering key length mismatch (should be %q)", ed.Name, ed.Key.ClusteringKeys)
   108  	}
   109  	for i, ck := range ed.Key.ClusteringKeys {
   110  		if md.ClusteringColumns[i].Name != ck.Name {
   111  			return fmt.Errorf("Table %q clustering key mismatch (column %d should be %q)", ed.Name, i+1, ck.Name)
   112  		}
   113  	}
   114  	return nil
   115  }
   116  func checkColumns(ed *dosa.EntityDefinition, md *gocql.TableMetadata, schemaErrors *RepairableSchemaMismatchError) {
   117  	// Check each column
   118  	for _, col := range ed.Columns {
   119  		_, ok := md.Columns[col.Name]
   120  		if !ok {
   121  			schemaErrors.MissingColumns = append(schemaErrors.MissingColumns, MissingColumn{Column: *col, Tablename: ed.Name})
   122  		}
   123  		// TODO: check column type
   124  	}
   125  }
   126  
   127  // compareStructToSchema compares a dosa EntityDefinition to the gocql TableMetadata
   128  // There are two main cases, one that we can fix by adding some columns and all the other mismatches that we can't fix
   129  func compareStructToSchema(ed *dosa.EntityDefinition, md *gocql.TableMetadata, schemaErrors *RepairableSchemaMismatchError) error {
   130  	if err := checkPartitionKeys(ed, md); err != nil {
   131  		return err
   132  	}
   133  	if err := checkClusteringKeys(ed, md); err != nil {
   134  		return err
   135  	}
   136  
   137  	checkColumns(ed, md, schemaErrors)
   138  
   139  	return nil
   140  
   141  }
   142  
   143  // CheckSchema verifies that the schema passed in the registered entities matches the database
   144  // This implementation only returns 0 or 1, since we are not storing schema
   145  // version information anywhere else
   146  func (c *Connector) CheckSchema(ctx context.Context, scope string, namePrefix string, ed []*dosa.EntityDefinition) (int32, error) {
   147  	schemaErrors := new(RepairableSchemaMismatchError)
   148  
   149  	// TODO: unfortunately, gocql doesn't have a way to pass the context to this operation :(
   150  	km, err := c.Session.KeyspaceMetadata(c.KsMapper.Keyspace(scope, namePrefix))
   151  	if err != nil {
   152  		return 0, err
   153  	}
   154  	for _, ed := range ed {
   155  		tableMetadata, ok := km.Tables[ed.Name]
   156  		if !ok {
   157  			schemaErrors.MissingTables = append(schemaErrors.MissingTables, ed.Name)
   158  			continue
   159  		}
   160  		if err := compareStructToSchema(ed, tableMetadata, schemaErrors); err != nil {
   161  			return 0, err
   162  		}
   163  
   164  	}
   165  	if schemaErrors.HasMissing() {
   166  		return 0, schemaErrors
   167  	}
   168  	return int32(1), nil
   169  }
   170  
   171  // UpsertSchema checks the schema and then updates it
   172  // We handle RepairableSchemaMismatchErrors, and return any
   173  // other error from CheckSchema
   174  func (c *Connector) UpsertSchema(ctx context.Context, scope string, namePrefix string, ed []*dosa.EntityDefinition) (*dosa.SchemaStatus, error) {
   175  	sr, err := c.CheckSchema(ctx, scope, namePrefix, ed)
   176  	if repairs, ok := err.(*RepairableSchemaMismatchError); ok {
   177  		for _, table := range repairs.MissingTables {
   178  			for _, def := range ed {
   179  				if def.Name == table {
   180  					err = c.createTable(c.KsMapper.Keyspace(scope, namePrefix), def)
   181  					if err != nil {
   182  						return nil, errors.Wrapf(err, "Unable to create table %q", def.Name)
   183  					}
   184  				}
   185  			}
   186  		}
   187  		for _, col := range repairs.MissingColumns {
   188  			if err := c.alterTable(c.KsMapper.Keyspace(scope, namePrefix), col.Tablename, col.Column); err != nil {
   189  				return nil, errors.Wrapf(err, "Unable to add column %q to table %q", col.Column.Name, col.Tablename)
   190  			}
   191  		}
   192  	}
   193  	return &dosa.SchemaStatus{Version: sr, Status: "ready"}, err
   194  }
   195  
   196  func (c *Connector) alterTable(keyspace, table string, col dosa.ColumnDefinition) error {
   197  	return c.Session.Query(alterTableString(keyspace, table, col)).Exec()
   198  }
   199  
   200  func alterTableString(keyspace, table string, col dosa.ColumnDefinition) string {
   201  	cts := &bytes.Buffer{}
   202  	fmt.Fprintf(cts, `ALTER TABLE "%s"."%s" ( ADD "%s" %s )`, keyspace, table, col.Name, cassandraType(col.Type))
   203  	return cts.String()
   204  }
   205  
   206  func (c *Connector) createTable(keyspace string, ed *dosa.EntityDefinition) error {
   207  	return c.Session.Query(createTableString(keyspace, ed)).Exec()
   208  }
   209  
   210  func createTableString(keyspace string, ed *dosa.EntityDefinition) string {
   211  	stmt, err := CreateStmt(
   212  		Keyspace(keyspace),
   213  		Table(ed.Name),
   214  		ColumnsWithType(ed.Columns),
   215  		PrimaryKey(ed.Key),
   216  	)
   217  
   218  	if err != nil {
   219  		panic(err)
   220  	}
   221  	return stmt
   222  }
   223  
   224  // ScopeExists is not implemented
   225  func (c *Connector) ScopeExists(ctx context.Context, scope string) (bool, error) {
   226  	panic("not implemented")
   227  }
   228  
   229  // CheckSchemaStatus is not implemented
   230  func (c *Connector) CheckSchemaStatus(context.Context, string, string, int32) (*dosa.SchemaStatus, error) {
   231  	panic("not implemented")
   232  }