github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/cassandra/cql.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  	"bytes"
    25  	"fmt"
    26  	"strconv"
    27  	"strings"
    28  	"text/template"
    29  
    30  	"github.com/uber-go/dosa"
    31  )
    32  
    33  const (
    34  	keyspace        = "Keyspace"
    35  	table           = "Table"
    36  	values          = "Values"
    37  	columns         = "Columns"
    38  	columnsWithType = "ColumnsWithType"
    39  	ifNotExist      = "IfNotExist"
    40  	limit           = "Limit"
    41  	conditions      = "Conditions"
    42  	key             = "Key"
    43  	insertTemplate  = `INSERT INTO {{.Keyspace}}.{{.Table}} ({{ColumnFunc .Columns ", "}}) VALUES ({{QuestionMark .Values ", "}}){{ExistsFunc .IfNotExist}};`
    44  	selectTemplate  = `SELECT {{ColumnFunc .Columns ", "}} FROM {{.Keyspace}}.{{.Table}}{{WhereFunc .Conditions}}{{ConditionsFunc .Conditions " AND "}}{{LimitFunc .Limit}};`
    45  	deleteTemplate  = `DELETE FROM {{.Keyspace}}.{{.Table}} WHERE {{ConditionsFunc .Conditions " AND "}};`
    46  	createTemplate  = `CREATE TABLE {{.Keyspace}}.{{.Table}} ({{ColumnWithType .ColumnsWithType ", "}}, PRIMARY KEY (({{PrimaryKeyClause .Key}}){{ClusteringKeyFunc .Key}})) WITH {{ClusteringOrderBy .Key}}COMPACTION = {'class':'LeveledCompactionStrategy'}`
    47  )
    48  
    49  var (
    50  	funcMap = template.FuncMap{
    51  		"ColumnFunc":        strings.Join,
    52  		"QuestionMark":      questionMarkFunc,
    53  		"ExistsFunc":        existsFunc,
    54  		"LimitFunc":         limitFunc,
    55  		"ConditionsFunc":    conditionsFunc,
    56  		"WhereFunc":         whereFunc,
    57  		"ColumnWithType":    columnWithTypeFunc,
    58  		"PrimaryKeyClause":  primaryKeyClauseFunc,
    59  		"ClusteringKeyFunc": clusteringKeyClauseFunc,
    60  		"ClusteringOrderBy": clusteringOrderByFunc,
    61  	}
    62  	insertTmpl = template.Must(template.New("insert").Funcs(funcMap).Parse(insertTemplate))
    63  	selectTmpl = template.Must(template.New("select").Funcs(funcMap).Parse(selectTemplate))
    64  	deleteTmpl = template.Must(template.New("delete").Funcs(funcMap).Parse(deleteTemplate))
    65  	createTmpl = template.Must(template.New("create").Funcs(funcMap).Parse(createTemplate))
    66  )
    67  
    68  func columnWithTypeFunc(ct []*dosa.ColumnDefinition, sep string) string {
    69  	cols := make([]string, len(ct))
    70  	for i, col := range ct {
    71  		cols[i] = strconv.Quote(col.Name) + " " + cassandraType(col.Type)
    72  	}
    73  	return strings.Join(cols, sep)
    74  }
    75  
    76  func primaryKeyClauseFunc(key *dosa.PrimaryKey) string {
    77  	partkeys := make([]string, len(key.PartitionKeys))
    78  	for i, partKey := range key.PartitionKeys {
    79  		partkeys[i] = strconv.Quote(partKey)
    80  	}
    81  	return strings.Join(partkeys, ",")
    82  }
    83  
    84  func clusteringKeyClauseFunc(key *dosa.PrimaryKey) string {
    85  	if len(key.ClusteringKeys) == 0 {
    86  		return ""
    87  	}
    88  	cluskeys := make([]string, len(key.ClusteringKeys))
    89  	for i, clusKey := range key.ClusteringKeys {
    90  		cluskeys[i] = strconv.Quote(clusKey.Name)
    91  	}
    92  	return "," + strings.Join(cluskeys, ",")
    93  }
    94  func clusteringOrderByFunc(key *dosa.PrimaryKey) string {
    95  	if len(key.ClusteringKeys) == 0 {
    96  		return ""
    97  	}
    98  	obc := make([]string, len(key.ClusteringKeys))
    99  	for i, ckc := range key.ClusteringKeys {
   100  		obc[i] = strconv.Quote(ckc.Name)
   101  		if ckc.Descending {
   102  			obc[i] += " DESC"
   103  		} else {
   104  			obc[i] += " ASC"
   105  		}
   106  	}
   107  	return "CLUSTERING ORDER BY (" + strings.Join(obc, ",") + ") AND "
   108  }
   109  
   110  func questionMarkFunc(qs []interface{}, sep string) string {
   111  	questions := make([]string, len(qs))
   112  	for i := range qs {
   113  		questions[i] = "?"
   114  	}
   115  	return strings.Join(questions, sep)
   116  }
   117  
   118  func existsFunc(exists bool) string {
   119  	if exists {
   120  		return " IF NOT EXISTS"
   121  	}
   122  	return ""
   123  }
   124  
   125  func limitFunc(num int) string {
   126  	if num > 0 {
   127  		return fmt.Sprintf(" LIMIT %d", num)
   128  	}
   129  	return ""
   130  }
   131  
   132  func conditionsFunc(conds []*ColumnCondition, sep string) string {
   133  	cstrs := make([]string, len(conds))
   134  	for i, cond := range conds {
   135  		sign := "="
   136  		switch cond.Condition.Op {
   137  		case dosa.Lt:
   138  			sign = "<"
   139  		case dosa.LtOrEq:
   140  			sign = "<="
   141  		case dosa.Gt:
   142  			sign = ">"
   143  		case dosa.GtOrEq:
   144  			sign = ">="
   145  		default:
   146  			sign = "="
   147  		}
   148  		cstrs[i] = fmt.Sprintf("%s%s?", strconv.Quote(cond.Name), sign)
   149  	}
   150  	return strings.Join(cstrs, sep)
   151  }
   152  
   153  func whereFunc(conds []*ColumnCondition) string {
   154  	if len(conds) > 0 {
   155  		return " WHERE "
   156  	}
   157  	return ""
   158  }
   159  
   160  // Option to compose a cql statement
   161  type Option map[string]interface{}
   162  
   163  // OptFunc is the interface to set option
   164  type OptFunc func(Option)
   165  
   166  // Keyspace sets the `keyspace` to the cql statement
   167  func Keyspace(v string) OptFunc {
   168  	return func(opt Option) {
   169  		opt[keyspace] = strconv.Quote(v)
   170  	}
   171  }
   172  
   173  // Table sets the `table` to the cql statement
   174  func Table(v string) OptFunc {
   175  	return func(opt Option) {
   176  		opt[table] = strconv.Quote(v)
   177  	}
   178  }
   179  
   180  // Columns sets the `columns` clause to the cql statement
   181  func Columns(v []string) OptFunc {
   182  	return func(opt Option) {
   183  		quoCs := make([]string, len(v))
   184  		for i, c := range v {
   185  			quoCs[i] = strconv.Quote(c)
   186  		}
   187  		opt[columns] = quoCs
   188  	}
   189  }
   190  
   191  // PrimaryKey sets the primary key structure for create table
   192  func PrimaryKey(pk *dosa.PrimaryKey) OptFunc {
   193  	return func(opt Option) {
   194  		opt[key] = pk
   195  	}
   196  }
   197  
   198  // ColumnsWithType sets the column definitions for each column
   199  // which is needed by create table
   200  func ColumnsWithType(cols []*dosa.ColumnDefinition) OptFunc {
   201  	return func(opt Option) {
   202  		opt[columnsWithType] = cols
   203  	}
   204  }
   205  
   206  // Values sets the `values` clause to the cql statement
   207  func Values(v interface{}) OptFunc {
   208  	return func(opt Option) {
   209  		opt[values] = v
   210  	}
   211  }
   212  
   213  // IfNotExist sets the `if not exist` clause to the cql statement
   214  func IfNotExist(v interface{}) OptFunc {
   215  	return func(opt Option) {
   216  		opt[ifNotExist] = v
   217  	}
   218  }
   219  
   220  // Limit sets the `limit` to the cql statement
   221  func Limit(v interface{}) OptFunc {
   222  	return func(opt Option) {
   223  		opt[limit] = v
   224  	}
   225  }
   226  
   227  // Conditions set the `where` clause to the cql statement
   228  func Conditions(v interface{}) OptFunc {
   229  	return func(opt Option) {
   230  		opt[conditions] = v
   231  	}
   232  }
   233  
   234  // InsertStmt creates insert statement
   235  func InsertStmt(opts ...OptFunc) (string, error) {
   236  	var bb bytes.Buffer
   237  	option := Option{
   238  		ifNotExist: false,
   239  	}
   240  	for _, opt := range opts {
   241  		opt(option)
   242  	}
   243  	err := insertTmpl.Execute(&bb, option)
   244  	return bb.String(), err
   245  }
   246  
   247  // SelectStmt creates select statement
   248  func SelectStmt(opts ...OptFunc) (string, error) {
   249  	var bb bytes.Buffer
   250  	option := Option{
   251  		limit: 0,
   252  	}
   253  	for _, opt := range opts {
   254  		opt(option)
   255  	}
   256  	err := selectTmpl.Execute(&bb, option)
   257  	return bb.String(), err
   258  }
   259  
   260  // DeleteStmt creates delete statement
   261  func DeleteStmt(opts ...OptFunc) (string, error) {
   262  	var bb bytes.Buffer
   263  	option := Option{}
   264  	for _, opt := range opts {
   265  		opt(option)
   266  	}
   267  	err := deleteTmpl.Execute(&bb, option)
   268  	return bb.String(), err
   269  }
   270  
   271  // CreateStmt creates a create statement
   272  func CreateStmt(opts ...OptFunc) (string, error) {
   273  	var bb bytes.Buffer
   274  	option := Option{}
   275  	for _, opt := range opts {
   276  		opt(option)
   277  	}
   278  	err := createTmpl.Execute(&bb, option)
   279  	return bb.String(), err
   280  }
   281  
   282  func cassandraType(t dosa.Type) string {
   283  	switch t {
   284  	case dosa.TUUID:
   285  		return "uuid"
   286  	case dosa.String:
   287  		return "text"
   288  	case dosa.Blob:
   289  		return "blob"
   290  	case dosa.Int32:
   291  		return "int"
   292  	case dosa.Int64:
   293  		return "bigint"
   294  	case dosa.Timestamp:
   295  		return "timestamp"
   296  	case dosa.Bool:
   297  		return "boolean"
   298  	case dosa.Double:
   299  		return "double"
   300  	}
   301  	panic(t)
   302  }