vitess.io/vitess@v0.16.2/go/mysql/binlog_event_json.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package mysql 18 19 import ( 20 "encoding/binary" 21 "fmt" 22 "math" 23 24 "vitess.io/vitess/go/vt/log" 25 26 "github.com/spyzhov/ajson" 27 28 querypb "vitess.io/vitess/go/vt/proto/query" 29 ) 30 31 /* 32 33 References: 34 35 * C source of mysql json data type implementation 36 https://fossies.org/linux/mysql/sql/json_binary.cc 37 38 * nice description of MySQL's json representation 39 https://lafengnan.gitbooks.io/blog/content/mysql/chapter2.html 40 41 * java/python connector links: useful for test cases and reverse engineering 42 https://github.com/shyiko/mysql-binlog-connector-java/pull/119/files 43 https://github.com/noplay/python-mysql-replication/blob/175df28cc8b536a68522ff9b09dc5440adad6094/pymysqlreplication/packet.py 44 45 */ 46 47 // region debug-only 48 // TODO remove once the json refactor is tested live 49 var jsonDebug = false 50 51 func jlog(tpl string, vals ...any) { 52 if !jsonDebug { 53 return 54 } 55 log.Infof("JSON:"+tpl+"\n", vals...) 56 _ = printASCIIBytes 57 } 58 59 func printASCIIBytes(data []byte) { 60 if !jsonDebug { 61 return 62 } 63 s := "" 64 for _, c := range data { 65 if c < 127 && c > 32 { 66 s += fmt.Sprintf("%c ", c) 67 } else { 68 s += fmt.Sprintf("%02d ", c) 69 } 70 } 71 log.Infof("[%s]", s) 72 } 73 74 // only used for logging/debugging 75 var jsonTypeToName = map[uint]string{ 76 jsonSmallObject: "sObject", 77 jsonLargeObject: "lObject", 78 jsonSmallArray: "sArray", 79 jsonLargeArray: "lArray", 80 jsonLiteral: "literal", 81 jsonInt16: "int16", 82 jsonUint16: "uint16", 83 jsonInt32: "int32", 84 jsonUint32: "uint32", 85 jsonInt64: "int64", 86 jsonUint64: "uint64", 87 jsonDouble: "double", //0x0b 88 jsonString: "string", //0x0c a utf8mb4 string 89 jsonOpaque: "opaque", //0x0f "custom" data 90 } 91 92 func jsonDataTypeToString(typ uint) string { 93 sType, ok := jsonTypeToName[typ] 94 if !ok { 95 return "undefined" 96 } 97 return sType 98 } 99 100 //endregion 101 102 // provides the single API function, used to convert json from binary format used in binlogs to a string representation 103 func getJSONValue(data []byte) (string, error) { 104 var ast *ajson.Node 105 var err error 106 if len(data) == 0 { 107 ast = ajson.NullNode("") 108 } else { 109 ast, err = binlogJSON.parse(data) 110 if err != nil { 111 return "", err 112 } 113 } 114 bytes, err := ajson.Marshal(ast) 115 if err != nil { 116 return "", err 117 } 118 return string(bytes), nil 119 } 120 121 var binlogJSON *BinlogJSON 122 123 func init() { 124 binlogJSON = &BinlogJSON{ 125 plugins: make(map[jsonDataType]jsonPlugin), 126 } 127 } 128 129 //region plugin manager 130 131 // BinlogJSON contains the plugins for all json types and methods for parsing the 132 // binary json representation of a specific type from the binlog 133 type BinlogJSON struct { 134 plugins map[jsonDataType]jsonPlugin 135 } 136 137 // parse decodes a value from the binlog 138 func (jh *BinlogJSON) parse(data []byte) (node *ajson.Node, err error) { 139 // pos keeps track of the offset of the current node being parsed 140 pos := 0 141 typ := data[pos] 142 jlog("Top level object is type %s\n", jsonDataTypeToString(uint(typ))) 143 pos++ 144 return jh.getNode(jsonDataType(typ), data, pos) 145 } 146 147 // each plugin registers itself in init()s 148 func (jh *BinlogJSON) register(typ jsonDataType, Plugin jsonPlugin) { 149 jh.plugins[typ] = Plugin 150 } 151 152 // gets the node at this position 153 func (jh *BinlogJSON) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 154 Plugin := jh.plugins[typ] 155 if Plugin == nil { 156 return nil, fmt.Errorf("plugin not found for type %d", typ) 157 } 158 return Plugin.getNode(typ, data, pos) 159 } 160 161 //endregion 162 163 //region enums 164 165 // jsonDataType has the values used in the mysql json binary representation to denote types. 166 // We have string, literal(true/false/null), number, object or array types. 167 // large object => doc size > 64K: you get pointers instead of inline values. 168 type jsonDataType byte 169 170 // type mapping as defined by the mysql json representation 171 const ( 172 jsonSmallObject = 0 173 jsonLargeObject = 1 174 jsonSmallArray = 2 175 jsonLargeArray = 3 176 jsonLiteral = 4 177 jsonInt16 = 5 178 jsonUint16 = 6 179 jsonInt32 = 7 180 jsonUint32 = 8 181 jsonInt64 = 9 182 jsonUint64 = 10 //0x0a 183 jsonDouble = 11 //0x0b 184 jsonString = 12 //0x0c a utf8mb4 string 185 jsonOpaque = 15 //0x0f "custom" data 186 ) 187 188 // literals in the binary json format can be one of three types: null, true, false 189 type jsonDataLiteral byte 190 191 // this is how mysql maps the three literals in the binlog 192 const ( 193 jsonNullLiteral = '\x00' 194 jsonTrueLiteral = '\x01' 195 jsonFalseLiteral = '\x02' 196 ) 197 198 //endregion 199 200 //region util funcs 201 202 // in objects and arrays some values are inlined, other types have offsets into the raw data. 203 // literals (true/false/null) and 16bit integers are always inlined. 204 // for large documents 32bit integers are also inlined. 205 // principle is that two byte values are inlined in "small", and four byte in "large" docs 206 func isInline(typ jsonDataType, large bool) bool { 207 switch typ { 208 case jsonLiteral, jsonInt16, jsonUint16: 209 return true 210 case jsonInt32, jsonUint32: 211 if large { 212 return true 213 } 214 } 215 return false 216 } 217 218 // readInt returns either a 32-bit or a 16-bit int from the passed buffer. Which one it is, 219 // depends on whether the document is "large" or not. 220 // JSON documents stored are considered "large" if the size of the stored json document is 221 // more than 64K bytes. Values of non-inlined types are stored as offsets into the document. 222 // The int returned is either an (i) offset into the raw data, (ii) count of elements, or (iii) size of the represented data structure. 223 // (This design decision allows a fixed number of bytes to be used for representing object keys and array indices.) 224 // readInt also returns the new position (by advancing the position by the number of bytes read). 225 func readInt(data []byte, pos int, large bool) (int, int) { 226 if large { 227 return int(data[pos]) + 228 int(data[pos+1])<<8 + 229 int(data[pos+2])<<16 + 230 int(data[pos+3])<<24, 231 pos + 4 232 } 233 return int(data[pos]) + 234 int(data[pos+1])<<8, pos + 2 235 } 236 237 // readVariableLength implements the logic to decode the length 238 // of an arbitrarily long string as implemented by the mysql server 239 // https://github.com/mysql/mysql-server/blob/5.7/sql/json_binary.cc#L234 240 // https://github.com/mysql/mysql-server/blob/8.0/sql/json_binary.cc#L283 241 // readVariableLength also returns the new position (by advancing the position by the number of bytes read). 242 func readVariableLength(data []byte, pos int) (int, int) { 243 var bb byte 244 var length int 245 var idx byte 246 for { 247 bb = data[pos] 248 pos++ 249 length |= int(bb&0x7f) << (7 * idx) 250 // if the high bit is 1, the integer value of the byte will be negative. 251 // high bit of 1 signifies that the next byte is part of the length encoding. 252 if int8(bb) >= 0 { 253 break 254 } 255 idx++ 256 } 257 return length, pos 258 } 259 260 // getElem returns the json value found inside json objects and arrays at the provided position 261 func getElem(data []byte, pos int, large bool) (*ajson.Node, int, error) { 262 var elem *ajson.Node 263 var err error 264 var offset int 265 typ := jsonDataType(data[pos]) 266 pos++ 267 if isInline(typ, large) { 268 elem, err = binlogJSON.getNode(typ, data, pos) 269 if err != nil { 270 return nil, 0, err 271 } 272 if large { 273 pos += 4 274 } else { 275 pos += 2 276 } 277 } else { 278 offset, pos = readInt(data, pos, large) 279 if offset >= len(data) { // consistency check, should only come here is there is a bug in the code 280 log.Errorf("unable to decode element") 281 return nil, 0, fmt.Errorf("unable to decode element: %+v", data) 282 } 283 newData := data[offset:] 284 //newPos ignored because this is an offset into the "extra" section of the buffer 285 elem, err = binlogJSON.getNode(typ, newData, 1) 286 if err != nil { 287 return nil, 0, err 288 } 289 } 290 return elem, pos, nil 291 } 292 293 //endregion 294 295 // json sub-type interface 296 // one plugin for each sub-type, plugins are stateless and initialized on load via individual init() functions 297 type jsonPlugin interface { 298 getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) 299 } 300 301 type jsonPluginInfo struct { 302 name string 303 types []jsonDataType 304 } 305 306 //region int plugin 307 308 func init() { 309 newIntPlugin() 310 } 311 312 type intPlugin struct { 313 info *jsonPluginInfo 314 sizes map[jsonDataType]int 315 } 316 317 var _ jsonPlugin = (*intPlugin)(nil) 318 319 func (ipl intPlugin) getVal(typ jsonDataType, data []byte, pos int) (value int64) { 320 var val uint64 321 var val2 int64 322 size := ipl.sizes[typ] 323 for i := 0; i < size; i++ { 324 val = val + uint64(data[pos+i])<<(8*i) 325 } 326 switch typ { 327 case jsonInt16: 328 val2 = int64(int16(val)) 329 case jsonInt32: 330 val2 = int64(int32(val)) 331 case jsonInt64: 332 val2 = int64(val) 333 } 334 return val2 335 } 336 337 func (ipl intPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 338 val := ipl.getVal(typ, data, pos) 339 node = ajson.IntegerNode("", val) 340 return node, nil 341 } 342 343 func newIntPlugin() *intPlugin { 344 ipl := &intPlugin{ 345 info: &jsonPluginInfo{ 346 name: "Int", 347 types: []jsonDataType{jsonInt64, jsonInt32, jsonInt16}, 348 }, 349 sizes: make(map[jsonDataType]int), 350 } 351 ipl.sizes = map[jsonDataType]int{ 352 jsonInt64: 8, 353 jsonInt32: 4, 354 jsonInt16: 2, 355 } 356 for _, typ := range ipl.info.types { 357 binlogJSON.register(typ, ipl) 358 } 359 return ipl 360 } 361 362 //endregion 363 364 //region uint plugin 365 366 func init() { 367 newUintPlugin() 368 } 369 370 type uintPlugin struct { 371 info *jsonPluginInfo 372 sizes map[jsonDataType]int 373 } 374 375 var _ jsonPlugin = (*uintPlugin)(nil) 376 377 func (upl uintPlugin) getVal(typ jsonDataType, data []byte, pos int) (value uint64) { 378 var val uint64 379 var val2 uint64 380 size := upl.sizes[typ] 381 for i := 0; i < size; i++ { 382 val = val + uint64(data[pos+i])<<(8*i) 383 } 384 switch typ { 385 case jsonUint16: 386 val2 = uint64(uint16(val)) 387 case jsonUint32: 388 val2 = uint64(uint32(val)) 389 case jsonUint64: 390 val2 = val 391 } 392 return val2 393 } 394 395 func (upl uintPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 396 val := upl.getVal(typ, data, pos) 397 node = ajson.UnsignedIntegerNode("", val) 398 return node, nil 399 } 400 401 func newUintPlugin() *uintPlugin { 402 upl := &uintPlugin{ 403 info: &jsonPluginInfo{ 404 name: "Uint", 405 types: []jsonDataType{jsonUint16, jsonUint32, jsonUint64}, 406 }, 407 sizes: make(map[jsonDataType]int), 408 } 409 upl.sizes = map[jsonDataType]int{ 410 jsonUint64: 8, 411 jsonUint32: 4, 412 jsonUint16: 2, 413 } 414 for _, typ := range upl.info.types { 415 binlogJSON.register(typ, upl) 416 } 417 return upl 418 } 419 420 //endregion 421 422 //region float plugin 423 424 func init() { 425 newFloatPlugin() 426 } 427 428 type floatPlugin struct { 429 info *jsonPluginInfo 430 sizes map[jsonDataType]int 431 } 432 433 var _ jsonPlugin = (*floatPlugin)(nil) 434 435 func (flp floatPlugin) getVal(typ jsonDataType, data []byte, pos int) (value float64) { 436 var val uint64 437 var val2 float64 438 size := flp.sizes[typ] 439 for i := 0; i < size; i++ { 440 val = val + uint64(data[pos+i])<<(8*i) 441 } 442 switch typ { 443 case jsonDouble: 444 val2 = math.Float64frombits(val) 445 } 446 return val2 447 } 448 449 func (flp floatPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 450 val := flp.getVal(typ, data, pos) 451 node = ajson.NumericNode("", val) 452 return node, nil 453 } 454 455 func newFloatPlugin() *floatPlugin { 456 fp := &floatPlugin{ 457 info: &jsonPluginInfo{ 458 name: "Float", 459 types: []jsonDataType{jsonDouble}, 460 }, 461 sizes: make(map[jsonDataType]int), 462 } 463 fp.sizes = map[jsonDataType]int{ 464 jsonDouble: 8, 465 } 466 for _, typ := range fp.info.types { 467 binlogJSON.register(typ, fp) 468 } 469 return fp 470 } 471 472 //endregion 473 474 //region literal plugin 475 476 func init() { 477 newLiteralPlugin() 478 } 479 480 type literalPlugin struct { 481 info *jsonPluginInfo 482 } 483 484 var _ jsonPlugin = (*literalPlugin)(nil) 485 486 func (lpl literalPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 487 val := jsonDataLiteral(data[pos]) 488 switch val { 489 case jsonNullLiteral: 490 node = ajson.NullNode("") 491 case jsonTrueLiteral: 492 node = ajson.BoolNode("", true) 493 case jsonFalseLiteral: 494 node = ajson.BoolNode("", false) 495 default: 496 return nil, fmt.Errorf("unknown literal value %v", val) 497 } 498 return node, nil 499 } 500 501 func newLiteralPlugin() *literalPlugin { 502 lpl := &literalPlugin{ 503 info: &jsonPluginInfo{ 504 name: "Literal", 505 types: []jsonDataType{jsonLiteral}, 506 }, 507 } 508 binlogJSON.register(jsonLiteral, lpl) 509 return lpl 510 } 511 512 //endregion 513 514 //region opaque plugin 515 516 func init() { 517 newOpaquePlugin() 518 } 519 520 type opaquePlugin struct { 521 info *jsonPluginInfo 522 } 523 524 var _ jsonPlugin = (*opaquePlugin)(nil) 525 526 // other types are stored as catch-all opaque types: documentation on these is scarce. 527 // we currently know about (and support) date/time/datetime/decimal. 528 func (opl opaquePlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 529 dataType := data[pos] 530 start := 3 // account for length of stored value 531 end := start + 8 // all currently supported opaque data types are 8 bytes in size 532 switch dataType { 533 case TypeDate: 534 raw := binary.LittleEndian.Uint64(data[start:end]) 535 value := raw >> 24 536 yearMonth := (value >> 22) & 0x01ffff // 17 bits starting at 22nd 537 year := yearMonth / 13 538 month := yearMonth % 13 539 day := (value >> 17) & 0x1f // 5 bits starting at 17th 540 dateString := fmt.Sprintf("%04d-%02d-%02d", year, month, day) 541 node = ajson.StringNode("", dateString) 542 case TypeTime: 543 raw := binary.LittleEndian.Uint64(data[start:end]) 544 value := raw >> 24 545 hour := (value >> 12) & 0x03ff // 10 bits starting at 12th 546 minute := (value >> 6) & 0x3f // 6 bits starting at 6th 547 second := value & 0x3f // 6 bits starting at 0th 548 microSeconds := raw & 0xffffff // 24 lower bits 549 timeString := fmt.Sprintf("%02d:%02d:%02d.%06d", hour, minute, second, microSeconds) 550 node = ajson.StringNode("", timeString) 551 case TypeDateTime: 552 raw := binary.LittleEndian.Uint64(data[start:end]) 553 value := raw >> 24 554 yearMonth := (value >> 22) & 0x01ffff // 17 bits starting at 22nd 555 year := yearMonth / 13 556 month := yearMonth % 13 557 day := (value >> 17) & 0x1f // 5 bits starting at 17th 558 hour := (value >> 12) & 0x1f // 5 bits starting at 12th 559 minute := (value >> 6) & 0x3f // 6 bits starting at 6th 560 second := value & 0x3f // 6 bits starting at 0th 561 microSeconds := raw & 0xffffff // 24 lower bits 562 timeString := fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d.%06d", year, month, day, hour, minute, second, microSeconds) 563 node = ajson.StringNode("", timeString) 564 case TypeNewDecimal: 565 decimalData := data[start:end] 566 precision := decimalData[0] 567 scale := decimalData[1] 568 metadata := (uint16(precision) << 8) + uint16(scale) 569 val, _, err := CellValue(decimalData, 2, TypeNewDecimal, metadata, &querypb.Field{Type: querypb.Type_DECIMAL}) 570 if err != nil { 571 return nil, err 572 } 573 float, err := val.ToFloat64() 574 if err != nil { 575 return nil, err 576 } 577 node = ajson.NumericNode("", float) 578 default: 579 return nil, fmt.Errorf("opaque type %d is not supported yet, data %v", dataType, data[2:]) 580 } 581 return node, nil 582 } 583 584 func newOpaquePlugin() *opaquePlugin { 585 opl := &opaquePlugin{ 586 info: &jsonPluginInfo{ 587 name: "Opaque", 588 types: []jsonDataType{jsonOpaque}, 589 }, 590 } 591 binlogJSON.register(jsonOpaque, opl) 592 return opl 593 } 594 595 //endregion 596 597 //region string plugin 598 599 func init() { 600 newStringPlugin() 601 } 602 603 type stringPlugin struct { 604 info *jsonPluginInfo 605 } 606 607 var _ jsonPlugin = (*stringPlugin)(nil) 608 609 func (spl stringPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 610 size, pos := readVariableLength(data, pos) 611 node = ajson.StringNode("", string(data[pos:pos+size])) 612 613 return node, nil 614 } 615 616 func newStringPlugin() *stringPlugin { 617 spl := &stringPlugin{ 618 info: &jsonPluginInfo{ 619 name: "String", 620 types: []jsonDataType{jsonString}, 621 }, 622 } 623 binlogJSON.register(jsonString, spl) 624 return spl 625 } 626 627 //endregion 628 629 //region array plugin 630 631 func init() { 632 newArrayPlugin() 633 } 634 635 type arrayPlugin struct { 636 info *jsonPluginInfo 637 } 638 639 var _ jsonPlugin = (*arrayPlugin)(nil) 640 641 // arrays are stored thus: 642 // | type_identifier(one of [2,3]) | elem count | obj size | list of offsets+lengths of values | actual values | 643 func (apl arrayPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 644 jlog("JSON Array %s, len %d", jsonDataTypeToString(uint(typ)), len(data)) 645 var nodes []*ajson.Node 646 var elem *ajson.Node 647 var elementCount, size int 648 large := typ == jsonLargeArray 649 elementCount, pos = readInt(data, pos, large) 650 jlog("Array(%t): elem count: %d\n", large, elementCount) 651 size, pos = readInt(data, pos, large) 652 jlog("Array(%t): elem count: %d, size:%d\n", large, elementCount, size) 653 for i := 0; i < elementCount; i++ { 654 elem, pos, err = getElem(data, pos, large) 655 if err != nil { 656 return nil, err 657 } 658 nodes = append(nodes, elem) 659 jlog("Index is %d:%s", i, jsonDataTypeToString(uint(typ))) 660 } 661 node = ajson.ArrayNode("", nodes) 662 return node, nil 663 } 664 665 func newArrayPlugin() *arrayPlugin { 666 apl := &arrayPlugin{ 667 info: &jsonPluginInfo{ 668 name: "Array", 669 types: []jsonDataType{jsonSmallArray, jsonLargeArray}, 670 }, 671 } 672 binlogJSON.register(jsonSmallArray, apl) 673 binlogJSON.register(jsonLargeArray, apl) 674 return apl 675 } 676 677 //endregion 678 679 //region object plugin 680 681 func init() { 682 newObjectPlugin() 683 } 684 685 type objectPlugin struct { 686 info *jsonPluginInfo 687 } 688 689 var _ jsonPlugin = (*objectPlugin)(nil) 690 691 // objects are stored thus: 692 // | type_identifier(0/1) | elem count | obj size | list of offsets+lengths of keys | list of offsets+lengths of values | actual keys | actual values | 693 func (opl objectPlugin) getNode(typ jsonDataType, data []byte, pos int) (node *ajson.Node, err error) { 694 jlog("JSON Type is %s, len %d", jsonDataTypeToString(uint(typ)), len(data)) 695 696 // "large" decides number of bytes used to specify element count and total object size: 4 bytes for large, 2 for small 697 var large = typ == jsonLargeObject 698 699 var elementCount int // total number of elements (== keys) in this object map. (element can be another object: recursively handled) 700 var size int // total size of object 701 702 elementCount, pos = readInt(data, pos, large) 703 size, pos = readInt(data, pos, large) 704 jlog("Object: elem count: %d, size %d\n", elementCount, size) 705 706 keys := make([]string, elementCount) // stores all the keys in this object 707 for i := 0; i < elementCount; i++ { 708 var keyOffset int 709 var keyLength int 710 keyOffset, pos = readInt(data, pos, large) 711 keyLength, pos = readInt(data, pos, false) // keyLength is always a 16-bit int 712 713 keyOffsetStart := keyOffset + 1 714 // check that offsets are not out of bounds (can happen only if there is a bug in the parsing code) 715 if keyOffsetStart >= len(data) || keyOffsetStart+keyLength > len(data) { 716 log.Errorf("unable to decode object elements") 717 return nil, fmt.Errorf("unable to decode object elements: %v", data) 718 } 719 keys[i] = string(data[keyOffsetStart : keyOffsetStart+keyLength]) 720 } 721 jlog("Object keys: %+v", keys) 722 723 object := make(map[string]*ajson.Node) 724 var elem *ajson.Node 725 726 // get the value for each key 727 for i := 0; i < elementCount; i++ { 728 elem, pos, err = getElem(data, pos, large) 729 if err != nil { 730 return nil, err 731 } 732 object[keys[i]] = elem 733 jlog("Key is %s:%s", keys[i], jsonDataTypeToString(uint(typ))) 734 } 735 736 node = ajson.ObjectNode("", object) 737 return node, nil 738 } 739 740 func newObjectPlugin() *objectPlugin { 741 opl := &objectPlugin{ 742 info: &jsonPluginInfo{ 743 name: "Object", 744 types: []jsonDataType{jsonSmallObject, jsonLargeObject}, 745 }, 746 } 747 binlogJSON.register(jsonSmallObject, opl) 748 binlogJSON.register(jsonLargeObject, opl) 749 return opl 750 } 751 752 //endregion