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 }