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 }