github.com/Tri-stone/burrow@v0.25.0/vent/sqlsol/projection.go (about)

     1  package sqlsol
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/hyperledger/burrow/vent/types"
    15  	"github.com/pkg/errors"
    16  	"github.com/xeipuuv/gojsonschema"
    17  )
    18  
    19  // Projection contains EventTable, Event & Abi specifications
    20  type Projection struct {
    21  	Tables    types.EventTables
    22  	EventSpec types.EventSpec
    23  }
    24  
    25  // NewProjectionFromBytes creates a Projection from a stream of bytes
    26  func NewProjectionFromBytes(bs []byte) (*Projection, error) {
    27  	eventSpec := types.EventSpec{}
    28  
    29  	err := ValidateJSONEventSpec(bs)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	err = json.Unmarshal(bs, &eventSpec)
    35  	if err != nil {
    36  		return nil, errors.Wrap(err, "Error unmarshalling eventSpec")
    37  	}
    38  
    39  	return NewProjectionFromEventSpec(eventSpec)
    40  }
    41  
    42  // NewProjectionFromFolder creates a Projection from a folder containing spec files
    43  func NewProjectionFromFolder(specFileOrDirs ...string) (*Projection, error) {
    44  	eventSpec := types.EventSpec{}
    45  
    46  	const errHeader = "NewProjectionFromFolder():"
    47  
    48  	for _, dir := range specFileOrDirs {
    49  		err := filepath.Walk(dir, func(path string, _ os.FileInfo, err error) error {
    50  			if err != nil {
    51  				return fmt.Errorf("error walking event spec files location '%s': %v", dir, err)
    52  			}
    53  			if filepath.Ext(path) == ".json" {
    54  				bs, err := readFile(path)
    55  				if err != nil {
    56  					return fmt.Errorf("error reading spec file '%s': %v", path, err)
    57  				}
    58  
    59  				err = ValidateJSONEventSpec(bs)
    60  				if err != nil {
    61  					return fmt.Errorf("could not validate spec file '%s': %v", path, err)
    62  				}
    63  
    64  				fileEventSpec := types.EventSpec{}
    65  				err = json.Unmarshal(bs, &fileEventSpec)
    66  				if err != nil {
    67  					return fmt.Errorf("error reading spec file '%s': %v", path, err)
    68  				}
    69  
    70  				eventSpec = append(eventSpec, fileEventSpec...)
    71  			}
    72  
    73  			return nil
    74  		})
    75  		if err != nil {
    76  			return nil, fmt.Errorf("%s %v", errHeader, err)
    77  		}
    78  	}
    79  
    80  	return NewProjectionFromEventSpec(eventSpec)
    81  }
    82  
    83  // NewProjectionFromEventSpec receives a sqlsol event specification
    84  // and returns a pointer to a filled projection structure
    85  // that contains event types mapped to SQL column types
    86  // and Event tables structures with table and columns info
    87  func NewProjectionFromEventSpec(eventSpec types.EventSpec) (*Projection, error) {
    88  	// builds abi information from specification
    89  	tables := make(types.EventTables)
    90  
    91  	// obtain global field mappings to add to table definitions
    92  	globalFieldMappings := getGlobalFieldMappings()
    93  
    94  	for _, eventClass := range eventSpec {
    95  		// validate json structure
    96  		if err := eventClass.Validate(); err != nil {
    97  			return nil, fmt.Errorf("validation error on %v: %v", eventClass, err)
    98  		}
    99  
   100  		// build columns mapping
   101  		var columns []*types.SQLTableColumn
   102  		channels := make(map[string][]string)
   103  
   104  		// Add the global mappings
   105  		eventClass.FieldMappings = append(globalFieldMappings, eventClass.FieldMappings...)
   106  
   107  		i := 0
   108  		for _, mapping := range eventClass.FieldMappings {
   109  			sqlType, sqlTypeLength, err := getSQLType(mapping.Type, mapping.BytesToString)
   110  			if err != nil {
   111  				return nil, err
   112  			}
   113  
   114  			i++
   115  
   116  			// Update channels broadcast payload subsets with this column
   117  			for _, channel := range mapping.Notify {
   118  				channels[channel] = append(channels[channel], mapping.ColumnName)
   119  			}
   120  
   121  			columns = append(columns, &types.SQLTableColumn{
   122  				Name:    mapping.ColumnName,
   123  				Type:    sqlType,
   124  				Primary: mapping.Primary,
   125  				Length:  sqlTypeLength,
   126  			})
   127  		}
   128  
   129  		// Allow for compatible composition of tables
   130  		var err error
   131  		tables[eventClass.TableName], err = mergeTables(tables[eventClass.TableName],
   132  			&types.SQLTable{
   133  				Name:           eventClass.TableName,
   134  				NotifyChannels: channels,
   135  				Columns:        columns,
   136  			})
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  
   141  	}
   142  
   143  	// check if there are duplicated duplicated column names (for a given table)
   144  	colName := make(map[string]int)
   145  
   146  	for _, table := range tables {
   147  		for _, column := range table.Columns {
   148  			colName[table.Name+column.Name]++
   149  			if colName[table.Name+column.Name] > 1 {
   150  				return nil, fmt.Errorf("duplicated column name: '%s' in table '%s'", column.Name, table.Name)
   151  			}
   152  		}
   153  	}
   154  
   155  	return &Projection{
   156  		Tables:    tables,
   157  		EventSpec: eventSpec,
   158  	}, nil
   159  }
   160  
   161  // Get the column for a particular table and column name
   162  func (p *Projection) GetColumn(tableName, columnName string) (*types.SQLTableColumn, error) {
   163  	if table, ok := p.Tables[tableName]; ok {
   164  		column := table.GetColumn(columnName)
   165  		if column == nil {
   166  			return nil, fmt.Errorf("GetColumn: table '%s' has no column '%s'",
   167  				tableName, columnName)
   168  		}
   169  		return column, nil
   170  	}
   171  
   172  	return nil, fmt.Errorf("GetColumn: table does not exist projection: %s ", tableName)
   173  }
   174  
   175  func ValidateJSONEventSpec(bs []byte) error {
   176  	schemaLoader := gojsonschema.NewGoLoader(types.EventSpecSchema())
   177  	specLoader := gojsonschema.NewBytesLoader(bs)
   178  	result, err := gojsonschema.Validate(schemaLoader, specLoader)
   179  	if err != nil {
   180  		return fmt.Errorf("could not validate using JSONSchema: %v", err)
   181  	}
   182  
   183  	if !result.Valid() {
   184  		errs := make([]string, len(result.Errors()))
   185  		for i, err := range result.Errors() {
   186  			errs[i] = err.String()
   187  		}
   188  		return fmt.Errorf("EventSpec failed JSONSchema validation:\n%s", strings.Join(errs, "\n"))
   189  	}
   190  	return nil
   191  }
   192  
   193  // readFile opens a given file and reads it contents into a stream of bytes
   194  func readFile(file string) ([]byte, error) {
   195  	theFile, err := os.Open(file)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	defer theFile.Close()
   200  
   201  	byteValue, err := ioutil.ReadAll(theFile)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	return byteValue, nil
   207  }
   208  
   209  // getSQLType maps event input types with corresponding SQL column types
   210  // takes into account related solidity types info and element indexed or hashed
   211  func getSQLType(evmSignature string, bytesToString bool) (types.SQLColumnType, int, error) {
   212  	evmSignature = strings.ToLower(evmSignature)
   213  	re := regexp.MustCompile("[0-9]+")
   214  	typeSize, _ := strconv.Atoi(re.FindString(evmSignature))
   215  
   216  	switch {
   217  	// solidity address => sql varchar
   218  	case evmSignature == types.EventFieldTypeAddress:
   219  		return types.SQLColumnTypeVarchar, 40, nil
   220  		// solidity bool => sql bool
   221  	case evmSignature == types.EventFieldTypeBool:
   222  		return types.SQLColumnTypeBool, 0, nil
   223  		// solidity bytes => sql bytes
   224  		// bytesToString == true means there is a string in there so => sql varchar
   225  	case strings.HasPrefix(evmSignature, types.EventFieldTypeBytes):
   226  		if bytesToString {
   227  			return types.SQLColumnTypeVarchar, 40, nil
   228  		} else {
   229  			return types.SQLColumnTypeByteA, 0, nil
   230  		}
   231  		// solidity string => sql text
   232  	case evmSignature == types.EventFieldTypeString:
   233  		return types.SQLColumnTypeText, 0, nil
   234  		// solidity int or int256 => sql bigint
   235  		// solidity int <= 32 => sql int
   236  		// solidity int > 32 => sql numeric
   237  	case strings.HasPrefix(evmSignature, types.EventFieldTypeInt):
   238  		if typeSize == 0 || typeSize == 256 {
   239  			return types.SQLColumnTypeBigInt, 0, nil
   240  		}
   241  		if typeSize <= 32 {
   242  			return types.SQLColumnTypeInt, 0, nil
   243  		} else {
   244  			return types.SQLColumnTypeNumeric, 0, nil
   245  		}
   246  		// solidity uint or uint256 => sql bigint
   247  		// solidity uint <= 16 => sql int
   248  		// solidity uint > 16 => sql numeric
   249  	case strings.HasPrefix(evmSignature, types.EventFieldTypeUInt):
   250  		if typeSize == 0 || typeSize == 256 {
   251  			return types.SQLColumnTypeBigInt, 0, nil
   252  		}
   253  		if typeSize <= 16 {
   254  			return types.SQLColumnTypeInt, 0, nil
   255  		} else {
   256  			return types.SQLColumnTypeNumeric, 0, nil
   257  		}
   258  	default:
   259  		return -1, 0, fmt.Errorf("Don't know how to map evmSignature: %s ", evmSignature)
   260  	}
   261  }
   262  
   263  // getGlobalColumns returns global columns for event table structures,
   264  // these columns will be part of every SQL event table to relate data with source events
   265  func getGlobalFieldMappings() []*types.EventFieldMapping {
   266  	return []*types.EventFieldMapping{
   267  		{
   268  			ColumnName: types.SQLColumnLabelHeight,
   269  			Field:      types.BlockHeightLabel,
   270  			Type:       types.EventFieldTypeString,
   271  		},
   272  		{
   273  			ColumnName: types.SQLColumnLabelTxHash,
   274  			Field:      types.TxTxHashLabel,
   275  			Type:       types.EventFieldTypeString,
   276  		},
   277  		{
   278  			ColumnName: types.SQLColumnLabelEventType,
   279  			Field:      types.EventTypeLabel,
   280  			Type:       types.EventFieldTypeString,
   281  		},
   282  		{
   283  			ColumnName: types.SQLColumnLabelEventName,
   284  			Field:      types.EventNameLabel,
   285  			Type:       types.EventFieldTypeString,
   286  		},
   287  	}
   288  }
   289  
   290  // Merges tables a and b provided the intersection of their columns (by name) are identical
   291  func mergeTables(tables ...*types.SQLTable) (*types.SQLTable, error) {
   292  	table := &types.SQLTable{
   293  		NotifyChannels: make(map[string][]string),
   294  	}
   295  
   296  	columns := make(map[string]*types.SQLTableColumn)
   297  	notifications := make(map[string]map[string]struct{})
   298  
   299  	for _, t := range tables {
   300  		if t != nil {
   301  			table.Name = t.Name
   302  			for _, columnB := range t.Columns {
   303  				if columnA, ok := columns[columnB.Name]; ok {
   304  					if !columnA.Equals(columnB) {
   305  						return nil, fmt.Errorf("cannot merge event class tables for %s because of "+
   306  							"conflicting columns: %v and %v", t.Name, columnB, columnB)
   307  					}
   308  					// Just keep existing column from A - they match
   309  				} else {
   310  					// Add as new column
   311  					table.Columns = append(table.Columns, columnB)
   312  					columns[columnB.Name] = columnB
   313  				}
   314  			}
   315  			for channel, columnNames := range t.NotifyChannels {
   316  				for _, columnName := range columnNames {
   317  					if notifications[channel] == nil {
   318  						notifications[channel] = make(map[string]struct{})
   319  					}
   320  					notifications[channel][columnName] = struct{}{}
   321  				}
   322  			}
   323  		}
   324  	}
   325  
   326  	// Merge notification channels requested by specs
   327  	for channel, colMap := range notifications {
   328  		for columnName := range colMap {
   329  			table.NotifyChannels[channel] = append(table.NotifyChannels[channel], columnName)
   330  		}
   331  		sort.Strings(table.NotifyChannels[channel])
   332  	}
   333  
   334  	return table, nil
   335  }