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 }