1101 lines
26 KiB
Go
1101 lines
26 KiB
Go
// Copyright (c) 2015 The gocql Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package gocql
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// schema metadata for a keyspace
|
|
type KeyspaceMetadata struct {
|
|
Name string
|
|
DurableWrites bool
|
|
StrategyClass string
|
|
StrategyOptions map[string]interface{}
|
|
Tables map[string]*TableMetadata
|
|
}
|
|
|
|
// schema metadata for a table (a.k.a. column family)
|
|
type TableMetadata struct {
|
|
Keyspace string
|
|
Name string
|
|
KeyValidator string
|
|
Comparator string
|
|
DefaultValidator string
|
|
KeyAliases []string
|
|
ColumnAliases []string
|
|
ValueAlias string
|
|
PartitionKey []*ColumnMetadata
|
|
ClusteringColumns []*ColumnMetadata
|
|
Columns map[string]*ColumnMetadata
|
|
OrderedColumns []string
|
|
}
|
|
|
|
// schema metadata for a column
|
|
type ColumnMetadata struct {
|
|
Keyspace string
|
|
Table string
|
|
Name string
|
|
ComponentIndex int
|
|
Kind ColumnKind
|
|
Validator string
|
|
Type TypeInfo
|
|
ClusteringOrder string
|
|
Order ColumnOrder
|
|
Index ColumnIndexMetadata
|
|
}
|
|
|
|
// the ordering of the column with regard to its comparator
|
|
type ColumnOrder bool
|
|
|
|
const (
|
|
ASC ColumnOrder = false
|
|
DESC = true
|
|
)
|
|
|
|
type ColumnIndexMetadata struct {
|
|
Name string
|
|
Type string
|
|
Options map[string]interface{}
|
|
}
|
|
|
|
type ColumnKind int
|
|
|
|
const (
|
|
ColumnUnkownKind ColumnKind = iota
|
|
ColumnPartitionKey
|
|
ColumnClusteringKey
|
|
ColumnRegular
|
|
ColumnCompact
|
|
ColumnStatic
|
|
)
|
|
|
|
func (c ColumnKind) String() string {
|
|
switch c {
|
|
case ColumnPartitionKey:
|
|
return "partition_key"
|
|
case ColumnClusteringKey:
|
|
return "clustering_key"
|
|
case ColumnRegular:
|
|
return "regular"
|
|
case ColumnCompact:
|
|
return "compact"
|
|
case ColumnStatic:
|
|
return "static"
|
|
default:
|
|
return fmt.Sprintf("unknown_column_%d", c)
|
|
}
|
|
}
|
|
|
|
func (c *ColumnKind) UnmarshalCQL(typ TypeInfo, p []byte) error {
|
|
if typ.Type() != TypeVarchar {
|
|
return unmarshalErrorf("unable to marshall %s into ColumnKind, expected Varchar", typ)
|
|
}
|
|
|
|
kind, err := columnKindFromSchema(string(p))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*c = kind
|
|
|
|
return nil
|
|
}
|
|
|
|
func columnKindFromSchema(kind string) (ColumnKind, error) {
|
|
switch kind {
|
|
case "partition_key":
|
|
return ColumnPartitionKey, nil
|
|
case "clustering_key", "clustering":
|
|
return ColumnClusteringKey, nil
|
|
case "regular":
|
|
return ColumnRegular, nil
|
|
case "compact_value":
|
|
return ColumnCompact, nil
|
|
case "static":
|
|
return ColumnStatic, nil
|
|
default:
|
|
return -1, fmt.Errorf("unknown column kind: %q", kind)
|
|
}
|
|
}
|
|
|
|
// default alias values
|
|
const (
|
|
DEFAULT_KEY_ALIAS = "key"
|
|
DEFAULT_COLUMN_ALIAS = "column"
|
|
DEFAULT_VALUE_ALIAS = "value"
|
|
)
|
|
|
|
// queries the cluster for schema information for a specific keyspace
|
|
type schemaDescriber struct {
|
|
session *Session
|
|
mu sync.Mutex
|
|
|
|
cache map[string]*KeyspaceMetadata
|
|
}
|
|
|
|
// creates a session bound schema describer which will query and cache
|
|
// keyspace metadata
|
|
func newSchemaDescriber(session *Session) *schemaDescriber {
|
|
return &schemaDescriber{
|
|
session: session,
|
|
cache: map[string]*KeyspaceMetadata{},
|
|
}
|
|
}
|
|
|
|
// returns the cached KeyspaceMetadata held by the describer for the named
|
|
// keyspace.
|
|
func (s *schemaDescriber) getSchema(keyspaceName string) (*KeyspaceMetadata, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
metadata, found := s.cache[keyspaceName]
|
|
if !found {
|
|
// refresh the cache for this keyspace
|
|
err := s.refreshSchema(keyspaceName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metadata = s.cache[keyspaceName]
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
// clears the already cached keyspace metadata
|
|
func (s *schemaDescriber) clearSchema(keyspaceName string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
delete(s.cache, keyspaceName)
|
|
}
|
|
|
|
// forcibly updates the current KeyspaceMetadata held by the schema describer
|
|
// for a given named keyspace.
|
|
func (s *schemaDescriber) refreshSchema(keyspaceName string) error {
|
|
var err error
|
|
|
|
// query the system keyspace for schema data
|
|
// TODO retrieve concurrently
|
|
keyspace, err := getKeyspaceMetadata(s.session, keyspaceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tables, err := getTableMetadata(s.session, keyspaceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
columns, err := getColumnMetadata(s.session, keyspaceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// organize the schema data
|
|
compileMetadata(s.session.cfg.ProtoVersion, keyspace, tables, columns)
|
|
|
|
// update the cache
|
|
s.cache[keyspaceName] = keyspace
|
|
|
|
return nil
|
|
}
|
|
|
|
// "compiles" derived information about keyspace, table, and column metadata
|
|
// for a keyspace from the basic queried metadata objects returned by
|
|
// getKeyspaceMetadata, getTableMetadata, and getColumnMetadata respectively;
|
|
// Links the metadata objects together and derives the column composition of
|
|
// the partition key and clustering key for a table.
|
|
func compileMetadata(
|
|
protoVersion int,
|
|
keyspace *KeyspaceMetadata,
|
|
tables []TableMetadata,
|
|
columns []ColumnMetadata,
|
|
) {
|
|
keyspace.Tables = make(map[string]*TableMetadata)
|
|
for i := range tables {
|
|
tables[i].Columns = make(map[string]*ColumnMetadata)
|
|
|
|
keyspace.Tables[tables[i].Name] = &tables[i]
|
|
}
|
|
|
|
// add columns from the schema data
|
|
for i := range columns {
|
|
col := &columns[i]
|
|
// decode the validator for TypeInfo and order
|
|
if col.ClusteringOrder != "" { // Cassandra 3.x+
|
|
col.Type = getCassandraType(col.Validator)
|
|
col.Order = ASC
|
|
if col.ClusteringOrder == "desc" {
|
|
col.Order = DESC
|
|
}
|
|
} else {
|
|
validatorParsed := parseType(col.Validator)
|
|
col.Type = validatorParsed.types[0]
|
|
col.Order = ASC
|
|
if validatorParsed.reversed[0] {
|
|
col.Order = DESC
|
|
}
|
|
}
|
|
|
|
table, ok := keyspace.Tables[col.Table]
|
|
if !ok {
|
|
// if the schema is being updated we will race between seeing
|
|
// the metadata be complete. Potentially we should check for
|
|
// schema versions before and after reading the metadata and
|
|
// if they dont match try again.
|
|
continue
|
|
}
|
|
|
|
table.Columns[col.Name] = col
|
|
table.OrderedColumns = append(table.OrderedColumns, col.Name)
|
|
}
|
|
|
|
if protoVersion == protoVersion1 {
|
|
compileV1Metadata(tables)
|
|
} else {
|
|
compileV2Metadata(tables)
|
|
}
|
|
}
|
|
|
|
// Compiles derived information from TableMetadata which have had
|
|
// ColumnMetadata added already. V1 protocol does not return as much
|
|
// column metadata as V2+ (because V1 doesn't support the "type" column in the
|
|
// system.schema_columns table) so determining PartitionKey and ClusterColumns
|
|
// is more complex.
|
|
func compileV1Metadata(tables []TableMetadata) {
|
|
for i := range tables {
|
|
table := &tables[i]
|
|
|
|
// decode the key validator
|
|
keyValidatorParsed := parseType(table.KeyValidator)
|
|
// decode the comparator
|
|
comparatorParsed := parseType(table.Comparator)
|
|
|
|
// the partition key length is the same as the number of types in the
|
|
// key validator
|
|
table.PartitionKey = make([]*ColumnMetadata, len(keyValidatorParsed.types))
|
|
|
|
// V1 protocol only returns "regular" columns from
|
|
// system.schema_columns (there is no type field for columns)
|
|
// so the alias information is used to
|
|
// create the partition key and clustering columns
|
|
|
|
// construct the partition key from the alias
|
|
for i := range table.PartitionKey {
|
|
var alias string
|
|
if len(table.KeyAliases) > i {
|
|
alias = table.KeyAliases[i]
|
|
} else if i == 0 {
|
|
alias = DEFAULT_KEY_ALIAS
|
|
} else {
|
|
alias = DEFAULT_KEY_ALIAS + strconv.Itoa(i+1)
|
|
}
|
|
|
|
column := &ColumnMetadata{
|
|
Keyspace: table.Keyspace,
|
|
Table: table.Name,
|
|
Name: alias,
|
|
Type: keyValidatorParsed.types[i],
|
|
Kind: ColumnPartitionKey,
|
|
ComponentIndex: i,
|
|
}
|
|
|
|
table.PartitionKey[i] = column
|
|
table.Columns[alias] = column
|
|
}
|
|
|
|
// determine the number of clustering columns
|
|
size := len(comparatorParsed.types)
|
|
if comparatorParsed.isComposite {
|
|
if len(comparatorParsed.collections) != 0 ||
|
|
(len(table.ColumnAliases) == size-1 &&
|
|
comparatorParsed.types[size-1].Type() == TypeVarchar) {
|
|
size = size - 1
|
|
}
|
|
} else {
|
|
if !(len(table.ColumnAliases) != 0 || len(table.Columns) == 0) {
|
|
size = 0
|
|
}
|
|
}
|
|
|
|
table.ClusteringColumns = make([]*ColumnMetadata, size)
|
|
|
|
for i := range table.ClusteringColumns {
|
|
var alias string
|
|
if len(table.ColumnAliases) > i {
|
|
alias = table.ColumnAliases[i]
|
|
} else if i == 0 {
|
|
alias = DEFAULT_COLUMN_ALIAS
|
|
} else {
|
|
alias = DEFAULT_COLUMN_ALIAS + strconv.Itoa(i+1)
|
|
}
|
|
|
|
order := ASC
|
|
if comparatorParsed.reversed[i] {
|
|
order = DESC
|
|
}
|
|
|
|
column := &ColumnMetadata{
|
|
Keyspace: table.Keyspace,
|
|
Table: table.Name,
|
|
Name: alias,
|
|
Type: comparatorParsed.types[i],
|
|
Order: order,
|
|
Kind: ColumnClusteringKey,
|
|
ComponentIndex: i,
|
|
}
|
|
|
|
table.ClusteringColumns[i] = column
|
|
table.Columns[alias] = column
|
|
}
|
|
|
|
if size != len(comparatorParsed.types)-1 {
|
|
alias := DEFAULT_VALUE_ALIAS
|
|
if len(table.ValueAlias) > 0 {
|
|
alias = table.ValueAlias
|
|
}
|
|
// decode the default validator
|
|
defaultValidatorParsed := parseType(table.DefaultValidator)
|
|
column := &ColumnMetadata{
|
|
Keyspace: table.Keyspace,
|
|
Table: table.Name,
|
|
Name: alias,
|
|
Type: defaultValidatorParsed.types[0],
|
|
Kind: ColumnRegular,
|
|
}
|
|
table.Columns[alias] = column
|
|
}
|
|
}
|
|
}
|
|
|
|
// The simpler compile case for V2+ protocol
|
|
func compileV2Metadata(tables []TableMetadata) {
|
|
for i := range tables {
|
|
table := &tables[i]
|
|
|
|
clusteringColumnCount := componentColumnCountOfType(table.Columns, ColumnClusteringKey)
|
|
table.ClusteringColumns = make([]*ColumnMetadata, clusteringColumnCount)
|
|
|
|
if table.KeyValidator != "" {
|
|
keyValidatorParsed := parseType(table.KeyValidator)
|
|
table.PartitionKey = make([]*ColumnMetadata, len(keyValidatorParsed.types))
|
|
} else { // Cassandra 3.x+
|
|
partitionKeyCount := componentColumnCountOfType(table.Columns, ColumnPartitionKey)
|
|
table.PartitionKey = make([]*ColumnMetadata, partitionKeyCount)
|
|
}
|
|
|
|
for _, columnName := range table.OrderedColumns {
|
|
column := table.Columns[columnName]
|
|
if column.Kind == ColumnPartitionKey {
|
|
table.PartitionKey[column.ComponentIndex] = column
|
|
} else if column.Kind == ColumnClusteringKey {
|
|
table.ClusteringColumns[column.ComponentIndex] = column
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns the count of coluns with the given "kind" value.
|
|
func componentColumnCountOfType(columns map[string]*ColumnMetadata, kind ColumnKind) int {
|
|
maxComponentIndex := -1
|
|
for _, column := range columns {
|
|
if column.Kind == kind && column.ComponentIndex > maxComponentIndex {
|
|
maxComponentIndex = column.ComponentIndex
|
|
}
|
|
}
|
|
return maxComponentIndex + 1
|
|
}
|
|
|
|
// query only for the keyspace metadata for the specified keyspace from system.schema_keyspace
|
|
func getKeyspaceMetadata(session *Session, keyspaceName string) (*KeyspaceMetadata, error) {
|
|
keyspace := &KeyspaceMetadata{Name: keyspaceName}
|
|
|
|
if session.useSystemSchema { // Cassandra 3.x+
|
|
const stmt = `
|
|
SELECT durable_writes, replication
|
|
FROM system_schema.keyspaces
|
|
WHERE keyspace_name = ?`
|
|
|
|
var replication map[string]string
|
|
|
|
iter := session.control.query(stmt, keyspaceName)
|
|
if iter.NumRows() == 0 {
|
|
return nil, ErrKeyspaceDoesNotExist
|
|
}
|
|
iter.Scan(&keyspace.DurableWrites, &replication)
|
|
err := iter.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error querying keyspace schema: %v", err)
|
|
}
|
|
|
|
keyspace.StrategyClass = replication["class"]
|
|
delete(replication, "class")
|
|
|
|
keyspace.StrategyOptions = make(map[string]interface{}, len(replication))
|
|
for k, v := range replication {
|
|
keyspace.StrategyOptions[k] = v
|
|
}
|
|
} else {
|
|
|
|
const stmt = `
|
|
SELECT durable_writes, strategy_class, strategy_options
|
|
FROM system.schema_keyspaces
|
|
WHERE keyspace_name = ?`
|
|
|
|
var strategyOptionsJSON []byte
|
|
|
|
iter := session.control.query(stmt, keyspaceName)
|
|
if iter.NumRows() == 0 {
|
|
return nil, ErrKeyspaceDoesNotExist
|
|
}
|
|
iter.Scan(&keyspace.DurableWrites, &keyspace.StrategyClass, &strategyOptionsJSON)
|
|
err := iter.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error querying keyspace schema: %v", err)
|
|
}
|
|
|
|
err = json.Unmarshal(strategyOptionsJSON, &keyspace.StrategyOptions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Invalid JSON value '%s' as strategy_options for in keyspace '%s': %v",
|
|
strategyOptionsJSON, keyspace.Name, err,
|
|
)
|
|
}
|
|
}
|
|
|
|
return keyspace, nil
|
|
}
|
|
|
|
// query for only the table metadata in the specified keyspace from system.schema_columnfamilies
|
|
func getTableMetadata(session *Session, keyspaceName string) ([]TableMetadata, error) {
|
|
|
|
var (
|
|
iter *Iter
|
|
scan func(iter *Iter, table *TableMetadata) bool
|
|
stmt string
|
|
|
|
keyAliasesJSON []byte
|
|
columnAliasesJSON []byte
|
|
)
|
|
|
|
if session.useSystemSchema { // Cassandra 3.x+
|
|
stmt = `
|
|
SELECT
|
|
table_name
|
|
FROM system_schema.tables
|
|
WHERE keyspace_name = ?`
|
|
|
|
switchIter := func() *Iter {
|
|
iter.Close()
|
|
stmt = `
|
|
SELECT
|
|
view_name
|
|
FROM system_schema.views
|
|
WHERE keyspace_name = ?`
|
|
iter = session.control.query(stmt, keyspaceName)
|
|
return iter
|
|
}
|
|
|
|
scan = func(iter *Iter, table *TableMetadata) bool {
|
|
r := iter.Scan(
|
|
&table.Name,
|
|
)
|
|
if !r {
|
|
iter = switchIter()
|
|
if iter != nil {
|
|
switchIter = func() *Iter { return nil }
|
|
r = iter.Scan(&table.Name)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
} else if session.cfg.ProtoVersion == protoVersion1 {
|
|
// we have key aliases
|
|
stmt = `
|
|
SELECT
|
|
columnfamily_name,
|
|
key_validator,
|
|
comparator,
|
|
default_validator,
|
|
key_aliases,
|
|
column_aliases,
|
|
value_alias
|
|
FROM system.schema_columnfamilies
|
|
WHERE keyspace_name = ?`
|
|
|
|
scan = func(iter *Iter, table *TableMetadata) bool {
|
|
return iter.Scan(
|
|
&table.Name,
|
|
&table.KeyValidator,
|
|
&table.Comparator,
|
|
&table.DefaultValidator,
|
|
&keyAliasesJSON,
|
|
&columnAliasesJSON,
|
|
&table.ValueAlias,
|
|
)
|
|
}
|
|
} else {
|
|
stmt = `
|
|
SELECT
|
|
columnfamily_name,
|
|
key_validator,
|
|
comparator,
|
|
default_validator
|
|
FROM system.schema_columnfamilies
|
|
WHERE keyspace_name = ?`
|
|
|
|
scan = func(iter *Iter, table *TableMetadata) bool {
|
|
return iter.Scan(
|
|
&table.Name,
|
|
&table.KeyValidator,
|
|
&table.Comparator,
|
|
&table.DefaultValidator,
|
|
)
|
|
}
|
|
}
|
|
|
|
iter = session.control.query(stmt, keyspaceName)
|
|
|
|
tables := []TableMetadata{}
|
|
table := TableMetadata{Keyspace: keyspaceName}
|
|
|
|
for scan(iter, &table) {
|
|
var err error
|
|
|
|
// decode the key aliases
|
|
if keyAliasesJSON != nil {
|
|
table.KeyAliases = []string{}
|
|
err = json.Unmarshal(keyAliasesJSON, &table.KeyAliases)
|
|
if err != nil {
|
|
iter.Close()
|
|
return nil, fmt.Errorf(
|
|
"Invalid JSON value '%s' as key_aliases for in table '%s': %v",
|
|
keyAliasesJSON, table.Name, err,
|
|
)
|
|
}
|
|
}
|
|
|
|
// decode the column aliases
|
|
if columnAliasesJSON != nil {
|
|
table.ColumnAliases = []string{}
|
|
err = json.Unmarshal(columnAliasesJSON, &table.ColumnAliases)
|
|
if err != nil {
|
|
iter.Close()
|
|
return nil, fmt.Errorf(
|
|
"Invalid JSON value '%s' as column_aliases for in table '%s': %v",
|
|
columnAliasesJSON, table.Name, err,
|
|
)
|
|
}
|
|
}
|
|
|
|
tables = append(tables, table)
|
|
table = TableMetadata{Keyspace: keyspaceName}
|
|
}
|
|
|
|
err := iter.Close()
|
|
if err != nil && err != ErrNotFound {
|
|
return nil, fmt.Errorf("Error querying table schema: %v", err)
|
|
}
|
|
|
|
return tables, nil
|
|
}
|
|
|
|
func (s *Session) scanColumnMetadataV1(keyspace string) ([]ColumnMetadata, error) {
|
|
// V1 does not support the type column, and all returned rows are
|
|
// of kind "regular".
|
|
const stmt = `
|
|
SELECT
|
|
columnfamily_name,
|
|
column_name,
|
|
component_index,
|
|
validator,
|
|
index_name,
|
|
index_type,
|
|
index_options
|
|
FROM system.schema_columns
|
|
WHERE keyspace_name = ?`
|
|
|
|
var columns []ColumnMetadata
|
|
|
|
rows := s.control.query(stmt, keyspace).Scanner()
|
|
for rows.Next() {
|
|
var (
|
|
column = ColumnMetadata{Keyspace: keyspace}
|
|
indexOptionsJSON []byte
|
|
)
|
|
|
|
// all columns returned by V1 are regular
|
|
column.Kind = ColumnRegular
|
|
|
|
err := rows.Scan(&column.Table,
|
|
&column.Name,
|
|
&column.ComponentIndex,
|
|
&column.Validator,
|
|
&column.Index.Name,
|
|
&column.Index.Type,
|
|
&indexOptionsJSON)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(indexOptionsJSON) > 0 {
|
|
err := json.Unmarshal(indexOptionsJSON, &column.Index.Options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Invalid JSON value '%s' as index_options for column '%s' in table '%s': %v",
|
|
indexOptionsJSON,
|
|
column.Name,
|
|
column.Table,
|
|
err)
|
|
}
|
|
}
|
|
|
|
columns = append(columns, column)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return columns, nil
|
|
}
|
|
|
|
func (s *Session) scanColumnMetadataV2(keyspace string) ([]ColumnMetadata, error) {
|
|
// V2+ supports the type column
|
|
const stmt = `
|
|
SELECT
|
|
columnfamily_name,
|
|
column_name,
|
|
component_index,
|
|
validator,
|
|
index_name,
|
|
index_type,
|
|
index_options,
|
|
type
|
|
FROM system.schema_columns
|
|
WHERE keyspace_name = ?`
|
|
|
|
var columns []ColumnMetadata
|
|
|
|
rows := s.control.query(stmt, keyspace).Scanner()
|
|
for rows.Next() {
|
|
var (
|
|
column = ColumnMetadata{Keyspace: keyspace}
|
|
indexOptionsJSON []byte
|
|
)
|
|
|
|
err := rows.Scan(&column.Table,
|
|
&column.Name,
|
|
&column.ComponentIndex,
|
|
&column.Validator,
|
|
&column.Index.Name,
|
|
&column.Index.Type,
|
|
&indexOptionsJSON,
|
|
&column.Kind,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(indexOptionsJSON) > 0 {
|
|
err := json.Unmarshal(indexOptionsJSON, &column.Index.Options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Invalid JSON value '%s' as index_options for column '%s' in table '%s': %v",
|
|
indexOptionsJSON,
|
|
column.Name,
|
|
column.Table,
|
|
err)
|
|
}
|
|
}
|
|
|
|
columns = append(columns, column)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return columns, nil
|
|
|
|
}
|
|
|
|
func (s *Session) scanColumnMetadataSystem(keyspace string) ([]ColumnMetadata, error) {
|
|
const stmt = `
|
|
SELECT
|
|
table_name,
|
|
column_name,
|
|
clustering_order,
|
|
type,
|
|
kind,
|
|
position
|
|
FROM system_schema.columns
|
|
WHERE keyspace_name = ?`
|
|
|
|
var columns []ColumnMetadata
|
|
|
|
rows := s.control.query(stmt, keyspace).Scanner()
|
|
for rows.Next() {
|
|
column := ColumnMetadata{Keyspace: keyspace}
|
|
|
|
err := rows.Scan(&column.Table,
|
|
&column.Name,
|
|
&column.ClusteringOrder,
|
|
&column.Validator,
|
|
&column.Kind,
|
|
&column.ComponentIndex,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
columns = append(columns, column)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(zariel): get column index info from system_schema.indexes
|
|
|
|
return columns, nil
|
|
}
|
|
|
|
// query for only the column metadata in the specified keyspace from system.schema_columns
|
|
func getColumnMetadata(session *Session, keyspaceName string) ([]ColumnMetadata, error) {
|
|
var (
|
|
columns []ColumnMetadata
|
|
err error
|
|
)
|
|
|
|
// Deal with differences in protocol versions
|
|
if session.cfg.ProtoVersion == 1 {
|
|
columns, err = session.scanColumnMetadataV1(keyspaceName)
|
|
} else if session.useSystemSchema { // Cassandra 3.x+
|
|
columns, err = session.scanColumnMetadataSystem(keyspaceName)
|
|
} else {
|
|
columns, err = session.scanColumnMetadataV2(keyspaceName)
|
|
}
|
|
|
|
if err != nil && err != ErrNotFound {
|
|
return nil, fmt.Errorf("Error querying column schema: %v", err)
|
|
}
|
|
|
|
return columns, nil
|
|
}
|
|
|
|
// type definition parser state
|
|
type typeParser struct {
|
|
input string
|
|
index int
|
|
}
|
|
|
|
// the type definition parser result
|
|
type typeParserResult struct {
|
|
isComposite bool
|
|
types []TypeInfo
|
|
reversed []bool
|
|
collections map[string]TypeInfo
|
|
}
|
|
|
|
// Parse the type definition used for validator and comparator schema data
|
|
func parseType(def string) typeParserResult {
|
|
parser := &typeParser{input: def}
|
|
return parser.parse()
|
|
}
|
|
|
|
const (
|
|
REVERSED_TYPE = "org.apache.cassandra.db.marshal.ReversedType"
|
|
COMPOSITE_TYPE = "org.apache.cassandra.db.marshal.CompositeType"
|
|
COLLECTION_TYPE = "org.apache.cassandra.db.marshal.ColumnToCollectionType"
|
|
LIST_TYPE = "org.apache.cassandra.db.marshal.ListType"
|
|
SET_TYPE = "org.apache.cassandra.db.marshal.SetType"
|
|
MAP_TYPE = "org.apache.cassandra.db.marshal.MapType"
|
|
)
|
|
|
|
// represents a class specification in the type def AST
|
|
type typeParserClassNode struct {
|
|
name string
|
|
params []typeParserParamNode
|
|
// this is the segment of the input string that defined this node
|
|
input string
|
|
}
|
|
|
|
// represents a class parameter in the type def AST
|
|
type typeParserParamNode struct {
|
|
name *string
|
|
class typeParserClassNode
|
|
}
|
|
|
|
func (t *typeParser) parse() typeParserResult {
|
|
// parse the AST
|
|
ast, ok := t.parseClassNode()
|
|
if !ok {
|
|
// treat this is a custom type
|
|
return typeParserResult{
|
|
isComposite: false,
|
|
types: []TypeInfo{
|
|
NativeType{
|
|
typ: TypeCustom,
|
|
custom: t.input,
|
|
},
|
|
},
|
|
reversed: []bool{false},
|
|
collections: nil,
|
|
}
|
|
}
|
|
|
|
// interpret the AST
|
|
if strings.HasPrefix(ast.name, COMPOSITE_TYPE) {
|
|
count := len(ast.params)
|
|
|
|
// look for a collections param
|
|
last := ast.params[count-1]
|
|
collections := map[string]TypeInfo{}
|
|
if strings.HasPrefix(last.class.name, COLLECTION_TYPE) {
|
|
count--
|
|
|
|
for _, param := range last.class.params {
|
|
// decode the name
|
|
var name string
|
|
decoded, err := hex.DecodeString(*param.name)
|
|
if err != nil {
|
|
Logger.Printf(
|
|
"Error parsing type '%s', contains collection name '%s' with an invalid format: %v",
|
|
t.input,
|
|
*param.name,
|
|
err,
|
|
)
|
|
// just use the provided name
|
|
name = *param.name
|
|
} else {
|
|
name = string(decoded)
|
|
}
|
|
collections[name] = param.class.asTypeInfo()
|
|
}
|
|
}
|
|
|
|
types := make([]TypeInfo, count)
|
|
reversed := make([]bool, count)
|
|
|
|
for i, param := range ast.params[:count] {
|
|
class := param.class
|
|
reversed[i] = strings.HasPrefix(class.name, REVERSED_TYPE)
|
|
if reversed[i] {
|
|
class = class.params[0].class
|
|
}
|
|
types[i] = class.asTypeInfo()
|
|
}
|
|
|
|
return typeParserResult{
|
|
isComposite: true,
|
|
types: types,
|
|
reversed: reversed,
|
|
collections: collections,
|
|
}
|
|
} else {
|
|
// not composite, so one type
|
|
class := *ast
|
|
reversed := strings.HasPrefix(class.name, REVERSED_TYPE)
|
|
if reversed {
|
|
class = class.params[0].class
|
|
}
|
|
typeInfo := class.asTypeInfo()
|
|
|
|
return typeParserResult{
|
|
isComposite: false,
|
|
types: []TypeInfo{typeInfo},
|
|
reversed: []bool{reversed},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (class *typeParserClassNode) asTypeInfo() TypeInfo {
|
|
if strings.HasPrefix(class.name, LIST_TYPE) {
|
|
elem := class.params[0].class.asTypeInfo()
|
|
return CollectionType{
|
|
NativeType: NativeType{
|
|
typ: TypeList,
|
|
},
|
|
Elem: elem,
|
|
}
|
|
}
|
|
if strings.HasPrefix(class.name, SET_TYPE) {
|
|
elem := class.params[0].class.asTypeInfo()
|
|
return CollectionType{
|
|
NativeType: NativeType{
|
|
typ: TypeSet,
|
|
},
|
|
Elem: elem,
|
|
}
|
|
}
|
|
if strings.HasPrefix(class.name, MAP_TYPE) {
|
|
key := class.params[0].class.asTypeInfo()
|
|
elem := class.params[1].class.asTypeInfo()
|
|
return CollectionType{
|
|
NativeType: NativeType{
|
|
typ: TypeMap,
|
|
},
|
|
Key: key,
|
|
Elem: elem,
|
|
}
|
|
}
|
|
|
|
// must be a simple type or custom type
|
|
info := NativeType{typ: getApacheCassandraType(class.name)}
|
|
if info.typ == TypeCustom {
|
|
// add the entire class definition
|
|
info.custom = class.input
|
|
}
|
|
return info
|
|
}
|
|
|
|
// CLASS := ID [ PARAMS ]
|
|
func (t *typeParser) parseClassNode() (node *typeParserClassNode, ok bool) {
|
|
t.skipWhitespace()
|
|
|
|
startIndex := t.index
|
|
|
|
name, ok := t.nextIdentifier()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
params, ok := t.parseParamNodes()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
endIndex := t.index
|
|
|
|
node = &typeParserClassNode{
|
|
name: name,
|
|
params: params,
|
|
input: t.input[startIndex:endIndex],
|
|
}
|
|
return node, true
|
|
}
|
|
|
|
// PARAMS := "(" PARAM { "," PARAM } ")"
|
|
// PARAM := [ PARAM_NAME ":" ] CLASS
|
|
// PARAM_NAME := ID
|
|
func (t *typeParser) parseParamNodes() (params []typeParserParamNode, ok bool) {
|
|
t.skipWhitespace()
|
|
|
|
// the params are optional
|
|
if t.index == len(t.input) || t.input[t.index] != '(' {
|
|
return nil, true
|
|
}
|
|
|
|
params = []typeParserParamNode{}
|
|
|
|
// consume the '('
|
|
t.index++
|
|
|
|
t.skipWhitespace()
|
|
|
|
for t.input[t.index] != ')' {
|
|
// look for a named param, but if no colon, then we want to backup
|
|
backupIndex := t.index
|
|
|
|
// name will be a hex encoded version of a utf-8 string
|
|
name, ok := t.nextIdentifier()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
hasName := true
|
|
|
|
// TODO handle '=>' used for DynamicCompositeType
|
|
|
|
t.skipWhitespace()
|
|
|
|
if t.input[t.index] == ':' {
|
|
// there is a name for this parameter
|
|
|
|
// consume the ':'
|
|
t.index++
|
|
|
|
t.skipWhitespace()
|
|
} else {
|
|
// no name, backup
|
|
hasName = false
|
|
t.index = backupIndex
|
|
}
|
|
|
|
// parse the next full parameter
|
|
classNode, ok := t.parseClassNode()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
if hasName {
|
|
params = append(
|
|
params,
|
|
typeParserParamNode{name: &name, class: *classNode},
|
|
)
|
|
} else {
|
|
params = append(
|
|
params,
|
|
typeParserParamNode{class: *classNode},
|
|
)
|
|
}
|
|
|
|
t.skipWhitespace()
|
|
|
|
if t.input[t.index] == ',' {
|
|
// consume the comma
|
|
t.index++
|
|
|
|
t.skipWhitespace()
|
|
}
|
|
}
|
|
|
|
// consume the ')'
|
|
t.index++
|
|
|
|
return params, true
|
|
}
|
|
|
|
func (t *typeParser) skipWhitespace() {
|
|
for t.index < len(t.input) && isWhitespaceChar(t.input[t.index]) {
|
|
t.index++
|
|
}
|
|
}
|
|
|
|
func isWhitespaceChar(c byte) bool {
|
|
return c == ' ' || c == '\n' || c == '\t'
|
|
}
|
|
|
|
// ID := LETTER { LETTER }
|
|
// LETTER := "0"..."9" | "a"..."z" | "A"..."Z" | "-" | "+" | "." | "_" | "&"
|
|
func (t *typeParser) nextIdentifier() (id string, found bool) {
|
|
startIndex := t.index
|
|
for t.index < len(t.input) && isIdentifierChar(t.input[t.index]) {
|
|
t.index++
|
|
}
|
|
if startIndex == t.index {
|
|
return "", false
|
|
}
|
|
return t.input[startIndex:t.index], true
|
|
}
|
|
|
|
func isIdentifierChar(c byte) bool {
|
|
return (c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
c == '-' ||
|
|
c == '+' ||
|
|
c == '.' ||
|
|
c == '_' ||
|
|
c == '&'
|
|
}
|