github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/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  	Spec   types.ProjectionSpec
    23  }
    24  
    25  // NewProjectionFromBytes creates a Projection from a stream of bytes
    26  func NewProjectionFromBytes(bs []byte) (*Projection, error) {
    27  	spec := types.ProjectionSpec{}
    28  
    29  	err := ValidateJSONSpec(bs)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	err = json.Unmarshal(bs, &spec)
    35  	if err != nil {
    36  		return nil, errors.Wrap(err, "Error unmarshalling spec")
    37  	}
    38  
    39  	return NewProjection(spec)
    40  }
    41  
    42  // NewProjectionFromFolder creates a Projection from a folder containing spec files
    43  func NewProjectionFromFolder(specFileOrDirs ...string) (*Projection, error) {
    44  	spec := types.ProjectionSpec{}
    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 = ValidateJSONSpec(bs)
    60  				if err != nil {
    61  					return fmt.Errorf("could not validate spec file '%s': %v", path, err)
    62  				}
    63  
    64  				fileEventSpec := types.ProjectionSpec{}
    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  				spec = append(spec, 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 NewProjection(spec)
    81  }
    82  
    83  // Takes 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 NewProjection(spec types.ProjectionSpec) (*Projection, error) {
    88  	// builds abi information from specification
    89  	tables := make(types.EventTables)
    90  
    91  	for _, eventClass := range spec {
    92  		// validate json structure
    93  		if err := eventClass.Validate(); err != nil {
    94  			return nil, fmt.Errorf("validation error on %v: %v", eventClass, err)
    95  		}
    96  
    97  		// build columns mapping
    98  		var columns []*types.SQLTableColumn
    99  		channels := make(map[string][]string)
   100  
   101  		// do we have a primary key
   102  		primary := false
   103  		for _, mapping := range eventClass.FieldMappings {
   104  			if mapping.Primary {
   105  				primary = true
   106  				break
   107  			}
   108  		}
   109  
   110  		if !primary && eventClass.DeleteMarkerField != "" {
   111  			return nil, fmt.Errorf("no DeleteMarkerField allowed if no primary key on %v", eventClass)
   112  		}
   113  
   114  		// Add the global mappings
   115  		if primary {
   116  			eventClass.FieldMappings = append(getGlobalFieldMappings(), eventClass.FieldMappings...)
   117  		} else {
   118  			eventClass.FieldMappings = append(getGlobalFieldMappingsLogMode(), eventClass.FieldMappings...)
   119  		}
   120  
   121  		i := 0
   122  		for _, mapping := range eventClass.FieldMappings {
   123  			var bytesMapping BytesMapping
   124  			if mapping.BytesToHex {
   125  				bytesMapping = BytesToHex
   126  			} else if mapping.BytesToString {
   127  				bytesMapping = BytesToString
   128  			}
   129  			sqlType, sqlTypeLength, err := getSQLType(mapping.Type, bytesMapping)
   130  			if err != nil {
   131  				return nil, err
   132  			}
   133  
   134  			i++
   135  
   136  			// Update channels broadcast payload subsets with this column
   137  			for _, channel := range mapping.Notify {
   138  				channels[channel] = append(channels[channel], mapping.ColumnName)
   139  			}
   140  
   141  			columns = append(columns, &types.SQLTableColumn{
   142  				Name:    mapping.ColumnName,
   143  				Type:    sqlType,
   144  				Primary: mapping.Primary,
   145  				Length:  sqlTypeLength,
   146  			})
   147  		}
   148  
   149  		// Allow for compatible composition of tables
   150  		var err error
   151  		tables[eventClass.TableName], err = mergeTables(tables[eventClass.TableName],
   152  			&types.SQLTable{
   153  				Name:           eventClass.TableName,
   154  				NotifyChannels: channels,
   155  				Columns:        columns,
   156  			})
   157  		if err != nil {
   158  			return nil, err
   159  		}
   160  
   161  	}
   162  
   163  	// check if there are duplicated duplicated column names (for a given table)
   164  	colName := make(map[string]int)
   165  
   166  	for _, table := range tables {
   167  		for _, column := range table.Columns {
   168  			colName[table.Name+column.Name]++
   169  			if colName[table.Name+column.Name] > 1 {
   170  				return nil, fmt.Errorf("duplicated column name: '%s' in table '%s'", column.Name, table.Name)
   171  			}
   172  		}
   173  	}
   174  
   175  	return &Projection{
   176  		Tables: tables,
   177  		Spec:   spec,
   178  	}, nil
   179  }
   180  
   181  // Get the column for a particular table and column name
   182  func (p *Projection) GetColumn(tableName, columnName string) (*types.SQLTableColumn, error) {
   183  	if table, ok := p.Tables[tableName]; ok {
   184  		column := table.GetColumn(columnName)
   185  		if column == nil {
   186  			return nil, fmt.Errorf("GetColumn: table '%s' has no column '%s'",
   187  				tableName, columnName)
   188  		}
   189  		return column, nil
   190  	}
   191  
   192  	return nil, fmt.Errorf("GetColumn: table does not exist projection: %s ", tableName)
   193  }
   194  
   195  func ValidateJSONSpec(bs []byte) error {
   196  	schemaLoader := gojsonschema.NewGoLoader(types.ProjectionSpecSchema())
   197  	specLoader := gojsonschema.NewBytesLoader(bs)
   198  	result, err := gojsonschema.Validate(schemaLoader, specLoader)
   199  	if err != nil {
   200  		return fmt.Errorf("could not validate using JSONSchema: %v", err)
   201  	}
   202  
   203  	if !result.Valid() {
   204  		errs := make([]string, len(result.Errors()))
   205  		for i, err := range result.Errors() {
   206  			errs[i] = err.String()
   207  		}
   208  		return fmt.Errorf("ProjectionSpec failed JSONSchema validation:\n%s", strings.Join(errs, "\n"))
   209  	}
   210  	return nil
   211  }
   212  
   213  // readFile opens a given file and reads it contents into a stream of bytes
   214  func readFile(file string) ([]byte, error) {
   215  	theFile, err := os.Open(file)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	defer theFile.Close()
   220  
   221  	byteValue, err := ioutil.ReadAll(theFile)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	return byteValue, nil
   227  }
   228  
   229  type BytesMapping int
   230  
   231  const (
   232  	BytesToBytes = iota
   233  	BytesToString
   234  	BytesToHex
   235  )
   236  
   237  // getSQLType maps event input types with corresponding SQL column types
   238  // takes into account related solidity types info and element indexed or hashed
   239  func getSQLType(evmSignature string, bytesMapping BytesMapping) (types.SQLColumnType, int, error) {
   240  	evmSignature = strings.ToLower(evmSignature)
   241  	re := regexp.MustCompile("[0-9]+")
   242  	typeSize, _ := strconv.Atoi(re.FindString(evmSignature))
   243  
   244  	switch {
   245  	// solidity address => sql varchar
   246  	case evmSignature == types.EventFieldTypeAddress:
   247  		return types.SQLColumnTypeVarchar, 40, nil
   248  		// solidity bool => sql bool
   249  	case evmSignature == types.EventFieldTypeBool:
   250  		return types.SQLColumnTypeBool, 0, nil
   251  		// solidity bytes => sql bytes
   252  		// bytesToString == true means there is a string in there so => sql varchar
   253  	case strings.HasPrefix(evmSignature, types.EventFieldTypeBytes):
   254  		switch bytesMapping {
   255  		case BytesToString:
   256  			return types.SQLColumnTypeVarchar, 32, nil
   257  		case BytesToHex:
   258  			return types.SQLColumnTypeVarchar, 64, nil
   259  		default:
   260  			return types.SQLColumnTypeByteA, 0, nil
   261  		}
   262  		// solidity string => sql text
   263  	case evmSignature == types.EventFieldTypeString:
   264  		return types.SQLColumnTypeText, 0, nil
   265  
   266  	case strings.HasPrefix(evmSignature, types.EventFieldTypeInt):
   267  		return evmIntegerSizeToSqlType(typeSize, true), 0, nil
   268  
   269  	case strings.HasPrefix(evmSignature, types.EventFieldTypeUInt):
   270  		return evmIntegerSizeToSqlType(typeSize, false), 0, nil
   271  	default:
   272  		return -1, 0, fmt.Errorf("do not know how to map evmSignature: %s ", evmSignature)
   273  	}
   274  }
   275  
   276  func evmIntegerSizeToSqlType(size int, signed bool) types.SQLColumnType {
   277  	// Unsized ints default to 256-bit
   278  	if size == 0 {
   279  		return types.SQLColumnTypeNumeric
   280  	}
   281  	// since SQL integers are signed we need an extra bit headroom so high-order bit of a uint isn't interpreted as
   282  	// the sign bit
   283  	if !signed {
   284  		size++
   285  	}
   286  	switch {
   287  	case size <= 32:
   288  		return types.SQLColumnTypeInt
   289  	case size <= 64:
   290  		return types.SQLColumnTypeBigInt
   291  	}
   292  	return types.SQLColumnTypeNumeric
   293  }
   294  
   295  // getGlobalColumns returns global columns for event table structures,
   296  // these columns will be part of every SQL event table to relate data with source events
   297  func getGlobalFieldMappings() []*types.EventFieldMapping {
   298  	return []*types.EventFieldMapping{
   299  		{
   300  			ColumnName: columns.ChainID,
   301  			Field:      types.ChainIDLabel,
   302  			Type:       types.EventFieldTypeString,
   303  		},
   304  		{
   305  			ColumnName: columns.Height,
   306  			Field:      types.BlockHeightLabel,
   307  			Type:       types.EventFieldTypeUInt,
   308  		},
   309  		{
   310  			ColumnName: columns.TxIndex,
   311  			Field:      types.TxIndexLabel,
   312  			Type:       types.EventFieldTypeUInt,
   313  		},
   314  		{
   315  			ColumnName: columns.EventIndex,
   316  			Field:      types.EventIndexLabel,
   317  			Type:       types.EventFieldTypeUInt,
   318  		},
   319  		{
   320  			ColumnName: columns.TxHash,
   321  			Field:      types.TxTxHashLabel,
   322  			Type:       types.EventFieldTypeString,
   323  		},
   324  		{
   325  			ColumnName: columns.EventType,
   326  			Field:      types.EventTypeLabel,
   327  			Type:       types.EventFieldTypeString,
   328  		},
   329  		{
   330  			ColumnName: columns.EventName,
   331  			Field:      types.EventNameLabel,
   332  			Type:       types.EventFieldTypeString,
   333  		},
   334  	}
   335  }
   336  
   337  // getGlobalColumns returns global columns for event table structures,
   338  // these columns will be part of every SQL event table to relate data with source events
   339  func getGlobalFieldMappingsLogMode() []*types.EventFieldMapping {
   340  	return []*types.EventFieldMapping{
   341  		{
   342  			ColumnName: columns.ChainID,
   343  			Field:      types.ChainIDLabel,
   344  			Type:       types.EventFieldTypeString,
   345  			Primary:    true,
   346  		},
   347  		{
   348  			ColumnName: columns.Height,
   349  			Field:      types.BlockHeightLabel,
   350  			Type:       types.EventFieldTypeUInt,
   351  			Primary:    true,
   352  		},
   353  		{
   354  			ColumnName: columns.TxIndex,
   355  			Field:      types.TxIndexLabel,
   356  			Type:       types.EventFieldTypeUInt,
   357  			Primary:    true,
   358  		},
   359  		{
   360  			ColumnName: columns.EventIndex,
   361  			Field:      types.EventIndexLabel,
   362  			Type:       types.EventFieldTypeUInt,
   363  			Primary:    true,
   364  		},
   365  		{
   366  			ColumnName: columns.TxHash,
   367  			Field:      types.TxTxHashLabel,
   368  			Type:       types.EventFieldTypeString,
   369  		},
   370  		{
   371  			ColumnName: columns.EventType,
   372  			Field:      types.EventTypeLabel,
   373  			Type:       types.EventFieldTypeString,
   374  		},
   375  		{
   376  			ColumnName: columns.EventName,
   377  			Field:      types.EventNameLabel,
   378  			Type:       types.EventFieldTypeString,
   379  		},
   380  	}
   381  }
   382  
   383  // Merges tables a and b provided the intersection of their columns (by name) are identical
   384  func mergeTables(tables ...*types.SQLTable) (*types.SQLTable, error) {
   385  	table := &types.SQLTable{
   386  		NotifyChannels: make(map[string][]string),
   387  	}
   388  
   389  	columns := make(map[string]*types.SQLTableColumn)
   390  	notifications := make(map[string]map[string]struct{})
   391  
   392  	for _, t := range tables {
   393  		if t != nil {
   394  			table.Name = t.Name
   395  			for _, columnB := range t.Columns {
   396  				if columnA, ok := columns[columnB.Name]; ok {
   397  					if !columnA.Equals(columnB) {
   398  						return nil, fmt.Errorf("cannot merge event class tables for %s because of "+
   399  							"conflicting columns: %v and %v", t.Name, columnA, columnB)
   400  					}
   401  					// Just keep existing column from A - they match
   402  				} else {
   403  					// Add as new column
   404  					table.Columns = append(table.Columns, columnB)
   405  					columns[columnB.Name] = columnB
   406  				}
   407  			}
   408  			for channel, columnNames := range t.NotifyChannels {
   409  				for _, columnName := range columnNames {
   410  					if notifications[channel] == nil {
   411  						notifications[channel] = make(map[string]struct{})
   412  					}
   413  					notifications[channel][columnName] = struct{}{}
   414  				}
   415  			}
   416  		}
   417  	}
   418  
   419  	// Merge notification channels requested by specs
   420  	for channel, colMap := range notifications {
   421  		for columnName := range colMap {
   422  			table.NotifyChannels[channel] = append(table.NotifyChannels[channel], columnName)
   423  		}
   424  		sort.Strings(table.NotifyChannels[channel])
   425  	}
   426  
   427  	return table, nil
   428  }