github.com/wanlay/gorm-dm8@v1.0.5/dmr/m.go (about) 1 /* 2 * Copyright (c) 2000-2018, 达梦数据库有限公司. 3 * All rights reserved. 4 */ 5 package dmr 6 7 import ( 8 "bytes" 9 "context" 10 "database/sql" 11 "database/sql/driver" 12 "fmt" 13 "sync/atomic" 14 15 "github.com/wanlay/gorm-dm8/dmr/parser" 16 "golang.org/x/text/encoding" 17 ) 18 19 type DmConnection struct { 20 filterable 21 22 dmConnector *DmConnector 23 Access *dm_build_332 24 stmtMap map[int32]*DmStatement 25 stmtPool []stmtPoolInfo 26 lastExecInfo *execRetInfo 27 lexer *parser.Lexer 28 encode encoding.Encoding 29 encodeBuffer *bytes.Buffer 30 transformReaderDst []byte 31 transformReaderSrc []byte 32 33 serverEncoding string 34 GlobalServerSeries int 35 ServerVersion string 36 Malini2 bool 37 Execute2 bool 38 LobEmptyCompOrcl bool 39 IsoLevel int32 40 ReadOnly bool 41 NewLobFlag bool 42 sslEncrypt int 43 MaxRowSize int32 44 DDLAutoCommit bool 45 BackslashEscape bool 46 SvrStat int32 47 SvrMode int32 48 ConstParaOpt bool 49 DbTimezone int16 50 LifeTimeRemainder int16 51 InstanceName string 52 Schema string 53 LastLoginIP string 54 LastLoginTime string 55 FailedAttempts int32 56 LoginWarningID int32 57 GraceTimeRemainder int32 58 Guid string 59 DbName string 60 StandbyHost string 61 StandbyPort int32 62 StandbyCount int32 63 SessionID int64 64 OracleDateLanguage byte 65 FormatDate string 66 FormatTimestamp string 67 FormatTimestampTZ string 68 FormatTime string 69 FormatTimeTZ string 70 Local bool 71 MsgVersion int32 72 TrxStatus int32 73 dscControl bool 74 trxFinish bool 75 sessionID int64 76 autoCommit bool 77 isBatch bool 78 79 watching bool 80 watcher chan<- context.Context 81 closech chan struct{} 82 finished chan<- struct{} 83 canceled atomicError 84 closed atomicBool 85 } 86 87 func (conn *DmConnection) setTrxFinish(status int32) { 88 switch status & Dm_build_722 { 89 case Dm_build_719, Dm_build_720, Dm_build_721: 90 conn.trxFinish = true 91 default: 92 conn.trxFinish = false 93 } 94 } 95 96 func (dmConn *DmConnection) init() { 97 if dmConn.dmConnector.stmtPoolMaxSize > 0 { 98 dmConn.stmtPool = make([]stmtPoolInfo, 0, dmConn.dmConnector.stmtPoolMaxSize) 99 } 100 101 dmConn.stmtMap = make(map[int32]*DmStatement) 102 dmConn.DbTimezone = 0 103 dmConn.GlobalServerSeries = 0 104 dmConn.MaxRowSize = 0 105 dmConn.LobEmptyCompOrcl = false 106 dmConn.ReadOnly = false 107 dmConn.DDLAutoCommit = false 108 dmConn.ConstParaOpt = false 109 dmConn.IsoLevel = -1 110 dmConn.sessionID = -1 111 dmConn.Malini2 = true 112 dmConn.NewLobFlag = true 113 dmConn.Execute2 = true 114 dmConn.serverEncoding = ENCODING_GB18030 115 dmConn.TrxStatus = Dm_build_670 116 dmConn.OracleDateLanguage = byte(Locale) 117 dmConn.lastExecInfo = NewExceInfo() 118 dmConn.MsgVersion = Dm_build_603 119 120 dmConn.idGenerator = dmConnIDGenerator 121 } 122 123 func (dmConn *DmConnection) reset() { 124 dmConn.DbTimezone = 0 125 dmConn.GlobalServerSeries = 0 126 dmConn.MaxRowSize = 0 127 dmConn.LobEmptyCompOrcl = false 128 dmConn.ReadOnly = false 129 dmConn.DDLAutoCommit = false 130 dmConn.ConstParaOpt = false 131 dmConn.IsoLevel = -1 132 dmConn.sessionID = -1 133 dmConn.Malini2 = true 134 dmConn.NewLobFlag = true 135 dmConn.Execute2 = true 136 dmConn.serverEncoding = ENCODING_GB18030 137 dmConn.TrxStatus = Dm_build_670 138 } 139 140 func (dc *DmConnection) checkClosed() error { 141 if dc.closed.IsSet() { 142 return driver.ErrBadConn 143 } 144 145 return nil 146 } 147 148 func (dc *DmConnection) executeInner(query string, execType int16) (interface{}, error) { 149 150 stmt, err := NewDmStmt(dc, query) 151 152 if err != nil { 153 return nil, err 154 } 155 156 if execType == Dm_build_687 { 157 defer stmt.close() 158 } 159 160 stmt.innerUsed = true 161 if stmt.dmConn.dmConnector.escapeProcess { 162 stmt.nativeSql, err = stmt.dmConn.escape(stmt.nativeSql, stmt.dmConn.dmConnector.keyWords) 163 if err != nil { 164 stmt.close() 165 return nil, err 166 } 167 } 168 169 var optParamList []OptParameter 170 171 if stmt.dmConn.ConstParaOpt { 172 optParamList = make([]OptParameter, 0) 173 stmt.nativeSql, optParamList, err = stmt.dmConn.execOpt(stmt.nativeSql, optParamList, stmt.dmConn.getServerEncoding()) 174 if err != nil { 175 stmt.close() 176 optParamList = nil 177 } 178 } 179 180 if execType == Dm_build_686 && dc.dmConnector.enRsCache { 181 rpv, err := rp.get(stmt, query) 182 if err != nil { 183 return nil, err 184 } 185 186 if rpv != nil { 187 stmt.execInfo = rpv.execInfo 188 dc.lastExecInfo = rpv.execInfo 189 return newDmRows(rpv.getResultSet(stmt)), nil 190 } 191 } 192 193 var info *execRetInfo 194 195 if optParamList != nil && len(optParamList) > 0 { 196 info, err = dc.Access.Dm_build_411(stmt, optParamList) 197 if err != nil { 198 stmt.nativeSql = query 199 info, err = dc.Access.Dm_build_417(stmt, execType) 200 } 201 } else { 202 info, err = dc.Access.Dm_build_417(stmt, execType) 203 } 204 205 if err != nil { 206 stmt.close() 207 return nil, err 208 } 209 dc.lastExecInfo = info 210 211 if info.hasResultSet { 212 return newDmRows(newInnerRows(0, stmt, info)), nil 213 } else { 214 return newDmResult(stmt, info), nil 215 } 216 } 217 218 func g2dbIsoLevel(isoLevel int32) int32 { 219 switch isoLevel { 220 case 1: 221 return Dm_build_674 222 case 2: 223 return Dm_build_675 224 case 4: 225 return Dm_build_676 226 case 6: 227 return Dm_build_677 228 default: 229 return -1 230 } 231 } 232 233 func (dc *DmConnection) Begin() (driver.Tx, error) { 234 if len(dc.filterChain.filters) == 0 { 235 return dc.begin() 236 } else { 237 return dc.filterChain.reset().DmConnectionBegin(dc) 238 } 239 } 240 241 func (dc *DmConnection) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { 242 if len(dc.filterChain.filters) == 0 { 243 return dc.beginTx(ctx, opts) 244 } 245 return dc.filterChain.reset().DmConnectionBeginTx(dc, ctx, opts) 246 } 247 248 func (dc *DmConnection) Commit() error { 249 if len(dc.filterChain.filters) == 0 { 250 return dc.commit() 251 } else { 252 return dc.filterChain.reset().DmConnectionCommit(dc) 253 } 254 } 255 256 func (dc *DmConnection) Rollback() error { 257 if len(dc.filterChain.filters) == 0 { 258 return dc.rollback() 259 } else { 260 return dc.filterChain.reset().DmConnectionRollback(dc) 261 } 262 } 263 264 func (dc *DmConnection) Close() error { 265 if len(dc.filterChain.filters) == 0 { 266 return dc.close() 267 } else { 268 return dc.filterChain.reset().DmConnectionClose(dc) 269 } 270 } 271 272 func (dc *DmConnection) Ping(ctx context.Context) error { 273 if len(dc.filterChain.filters) == 0 { 274 return dc.ping(ctx) 275 } else { 276 return dc.filterChain.reset().DmConnectionPing(dc, ctx) 277 } 278 } 279 280 func (dc *DmConnection) Exec(query string, args []driver.Value) (driver.Result, error) { 281 if len(dc.filterChain.filters) == 0 { 282 return dc.exec(query, args) 283 } 284 return dc.filterChain.reset().DmConnectionExec(dc, query, args) 285 } 286 287 func (dc *DmConnection) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { 288 if len(dc.filterChain.filters) == 0 { 289 return dc.execContext(ctx, query, args) 290 } 291 return dc.filterChain.reset().DmConnectionExecContext(dc, ctx, query, args) 292 } 293 294 func (dc *DmConnection) Query(query string, args []driver.Value) (driver.Rows, error) { 295 if len(dc.filterChain.filters) == 0 { 296 return dc.query(query, args) 297 } 298 return dc.filterChain.reset().DmConnectionQuery(dc, query, args) 299 } 300 301 func (dc *DmConnection) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { 302 if len(dc.filterChain.filters) == 0 { 303 return dc.queryContext(ctx, query, args) 304 } 305 return dc.filterChain.reset().DmConnectionQueryContext(dc, ctx, query, args) 306 } 307 308 func (dc *DmConnection) Prepare(query string) (driver.Stmt, error) { 309 if len(dc.filterChain.filters) == 0 { 310 return dc.prepare(query) 311 } 312 return dc.filterChain.reset().DmConnectionPrepare(dc, query) 313 } 314 315 func (dc *DmConnection) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { 316 if len(dc.filterChain.filters) == 0 { 317 return dc.prepareContext(ctx, query) 318 } 319 return dc.filterChain.reset().DmConnectionPrepareContext(dc, ctx, query) 320 } 321 322 func (dc *DmConnection) ResetSession(ctx context.Context) error { 323 if len(dc.filterChain.filters) == 0 { 324 return dc.resetSession(ctx) 325 } 326 return dc.filterChain.reset().DmConnectionResetSession(dc, ctx) 327 } 328 329 func (dc *DmConnection) CheckNamedValue(nv *driver.NamedValue) error { 330 if len(dc.filterChain.filters) == 0 { 331 return dc.checkNamedValue(nv) 332 } 333 return dc.filterChain.reset().DmConnectionCheckNamedValue(dc, nv) 334 } 335 336 func (dc *DmConnection) begin() (*DmConnection, error) { 337 return dc.beginTx(context.Background(), driver.TxOptions{driver.IsolationLevel(sql.LevelDefault), false}) 338 } 339 340 func (dc *DmConnection) beginTx(ctx context.Context, opts driver.TxOptions) (*DmConnection, error) { 341 if err := dc.watchCancel(ctx); err != nil { 342 return nil, err 343 } 344 defer dc.finish() 345 346 err := dc.checkClosed() 347 if err != nil { 348 return nil, err 349 } 350 351 dc.autoCommit = false 352 353 if sql.IsolationLevel(opts.Isolation) == sql.LevelDefault { 354 opts.Isolation = driver.IsolationLevel(sql.LevelReadCommitted) 355 } 356 357 dc.ReadOnly = opts.ReadOnly 358 359 if dc.IsoLevel == int32(opts.Isolation) { 360 return dc, nil 361 } 362 363 switch sql.IsolationLevel(opts.Isolation) { 364 case sql.LevelDefault, sql.LevelReadUncommitted: 365 return dc, nil 366 case sql.LevelReadCommitted, sql.LevelSerializable: 367 dc.IsoLevel = int32(opts.Isolation) 368 case sql.LevelRepeatableRead: 369 if dc.CompatibleMysql() { 370 dc.IsoLevel = int32(sql.LevelReadCommitted) 371 } else { 372 return nil, ECGO_INVALID_TRAN_ISOLATION.throw() 373 } 374 default: 375 return nil, ECGO_INVALID_TRAN_ISOLATION.throw() 376 } 377 378 err = dc.Access.Dm_build_471(dc) 379 if err != nil { 380 return nil, err 381 } 382 return dc, nil 383 } 384 385 func (dc *DmConnection) commit() error { 386 err := dc.checkClosed() 387 if err != nil { 388 return err 389 } 390 391 defer func() { 392 dc.autoCommit = dc.dmConnector.autoCommit 393 }() 394 395 if !dc.autoCommit { 396 err = dc.Access.Commit() 397 if err != nil { 398 return err 399 } 400 dc.trxFinish = true 401 return nil 402 } else if !dc.dmConnector.alwayseAllowCommit { 403 return ECGO_COMMIT_IN_AUTOCOMMIT_MODE.throw() 404 } 405 406 return nil 407 } 408 409 func (dc *DmConnection) rollback() error { 410 err := dc.checkClosed() 411 if err != nil { 412 return err 413 } 414 415 defer func() { 416 dc.autoCommit = dc.dmConnector.autoCommit 417 }() 418 419 if !dc.autoCommit { 420 err = dc.Access.Rollback() 421 if err != nil { 422 return err 423 } 424 dc.trxFinish = true 425 return nil 426 } else if !dc.dmConnector.alwayseAllowCommit { 427 return ECGO_ROLLBACK_IN_AUTOCOMMIT_MODE.throw() 428 } 429 430 return nil 431 } 432 433 func (dc *DmConnection) reconnect() error { 434 err := dc.Access.Close() 435 if err != nil { 436 return err 437 } 438 439 for _, stmt := range dc.stmtMap { 440 stmt.closed = true 441 for id, _ := range stmt.rsMap { 442 delete(stmt.rsMap, id) 443 } 444 } 445 446 if dc.stmtPool != nil { 447 dc.stmtPool = dc.stmtPool[:0] 448 } 449 450 dc.dmConnector.reConnection = dc 451 452 if dc.dmConnector.group != nil { 453 _, err = dc.dmConnector.group.connect(dc.dmConnector) 454 if err != nil { 455 return err 456 } 457 } else { 458 _, err = dc.dmConnector.connect(context.Background()) 459 } 460 461 for _, stmt := range dc.stmtMap { 462 err = dc.Access.Dm_build_389(stmt) 463 if err != nil { 464 return err 465 } 466 467 if stmt.paramCount > 0 { 468 err = stmt.prepare() 469 if err != nil { 470 return err 471 } 472 } 473 } 474 475 return nil 476 } 477 478 func (dc *DmConnection) cleanup() { 479 dc.close() 480 } 481 482 func (dc *DmConnection) close() error { 483 if !dc.closed.TrySet(true) { 484 return nil 485 } 486 487 close(dc.closech) 488 if dc.Access == nil { 489 return nil 490 } 491 492 dc.rollback() 493 494 for _, stmt := range dc.stmtMap { 495 stmt.free() 496 } 497 498 if dc.stmtPool != nil { 499 for _, spi := range dc.stmtPool { 500 dc.Access.Dm_build_394(spi.id) 501 } 502 dc.stmtPool = nil 503 } 504 505 dc.Access.Close() 506 507 return nil 508 } 509 510 func (dc *DmConnection) ping(ctx context.Context) error { 511 if err := dc.watchCancel(ctx); err != nil { 512 return err 513 } 514 defer dc.finish() 515 516 rows, err := dc.query("select 1", nil) 517 if err != nil { 518 return err 519 } 520 return rows.close() 521 } 522 523 func (dc *DmConnection) exec(query string, args []driver.Value) (*DmResult, error) { 524 err := dc.checkClosed() 525 if err != nil { 526 return nil, err 527 } 528 529 if args != nil && len(args) > 0 { 530 stmt, err := dc.prepare(query) 531 defer stmt.close() 532 if err != nil { 533 return nil, err 534 } 535 dc.lastExecInfo = stmt.execInfo 536 537 return stmt.exec(args) 538 } else { 539 r1, err := dc.executeInner(query, Dm_build_687) 540 if err != nil { 541 return nil, err 542 } 543 544 if r2, ok := r1.(*DmResult); ok { 545 return r2, nil 546 } else { 547 return nil, ECGO_NOT_EXEC_SQL.throw() 548 } 549 } 550 } 551 552 func (dc *DmConnection) execContext(ctx context.Context, query string, args []driver.NamedValue) (*DmResult, error) { 553 554 if err := dc.watchCancel(ctx); err != nil { 555 return nil, err 556 } 557 defer dc.finish() 558 559 err := dc.checkClosed() 560 if err != nil { 561 return nil, err 562 } 563 564 if args != nil && len(args) > 0 { 565 stmt, err := dc.prepare(query) 566 defer stmt.close() 567 if err != nil { 568 return nil, err 569 } 570 dc.lastExecInfo = stmt.execInfo 571 572 return stmt.execContext(ctx, args) 573 } else { 574 r1, err := dc.executeInner(query, Dm_build_687) 575 if err != nil { 576 return nil, err 577 } 578 579 if r2, ok := r1.(*DmResult); ok { 580 return r2, nil 581 } else { 582 return nil, ECGO_NOT_EXEC_SQL.throw() 583 } 584 } 585 } 586 587 func (dc *DmConnection) query(query string, args []driver.Value) (*DmRows, error) { 588 589 err := dc.checkClosed() 590 if err != nil { 591 return nil, err 592 } 593 594 if args != nil && len(args) > 0 { 595 stmt, err := dc.prepare(query) 596 if err != nil { 597 stmt.close() 598 return nil, err 599 } 600 dc.lastExecInfo = stmt.execInfo 601 602 stmt.innerUsed = true 603 return stmt.query(args) 604 605 } else { 606 r1, err := dc.executeInner(query, Dm_build_686) 607 if err != nil { 608 return nil, err 609 } 610 611 if r2, ok := r1.(*DmRows); ok { 612 return r2, nil 613 } else { 614 return nil, ECGO_NOT_QUERY_SQL.throw() 615 } 616 } 617 } 618 619 func (dc *DmConnection) queryContext(ctx context.Context, query string, args []driver.NamedValue) (*DmRows, error) { 620 if err := dc.watchCancel(ctx); err != nil { 621 return nil, err 622 } 623 defer dc.finish() 624 625 err := dc.checkClosed() 626 if err != nil { 627 return nil, err 628 } 629 630 if args != nil && len(args) > 0 { 631 stmt, err := dc.prepare(query) 632 if err != nil { 633 stmt.close() 634 return nil, err 635 } 636 dc.lastExecInfo = stmt.execInfo 637 638 stmt.innerUsed = true 639 return stmt.queryContext(ctx, args) 640 641 } else { 642 r1, err := dc.executeInner(query, Dm_build_686) 643 if err != nil { 644 return nil, err 645 } 646 647 if r2, ok := r1.(*DmRows); ok { 648 return r2, nil 649 } else { 650 return nil, ECGO_NOT_QUERY_SQL.throw() 651 } 652 } 653 654 } 655 656 func (dc *DmConnection) prepare(query string) (*DmStatement, error) { 657 err := dc.checkClosed() 658 if err != nil { 659 return nil, err 660 } 661 662 stmt, err := NewDmStmt(dc, query) 663 if err != nil { 664 return nil, err 665 } 666 667 err = stmt.prepare() 668 return stmt, err 669 } 670 671 func (dc *DmConnection) prepareContext(ctx context.Context, query string) (*DmStatement, error) { 672 if err := dc.watchCancel(ctx); err != nil { 673 return nil, err 674 } 675 defer dc.finish() 676 677 err := dc.checkClosed() 678 if err != nil { 679 return nil, err 680 } 681 682 stmt, err := dc.prepare(query) 683 if err != nil { 684 return nil, err 685 } 686 687 return stmt, nil 688 } 689 690 func (dc *DmConnection) resetSession(ctx context.Context) error { 691 err := dc.checkClosed() 692 if err != nil { 693 return err 694 } 695 696 for _, stmt := range dc.stmtMap { 697 stmt.inUse = false 698 } 699 700 return nil 701 } 702 703 func (dc *DmConnection) checkNamedValue(nv *driver.NamedValue) error { 704 var err error 705 var cvt = converter{dc, false} 706 nv.Value, err = cvt.ConvertValue(nv.Value) 707 dc.isBatch = cvt.isBatch 708 return err 709 } 710 711 func (dc *DmConnection) driverQuery(query string) (*DmStatement, *DmRows, error) { 712 stmt, err := NewDmStmt(dc, query) 713 if err != nil { 714 return nil, nil, err 715 } 716 stmt.innerUsed = true 717 stmt.innerExec = true 718 info, err := dc.Access.Dm_build_417(stmt, Dm_build_686) 719 if err != nil { 720 return nil, nil, err 721 } 722 dc.lastExecInfo = info 723 stmt.innerExec = false 724 return stmt, newDmRows(newInnerRows(0, stmt, info)), nil 725 } 726 727 func (dc *DmConnection) getIndexOnEPGroup() int32 { 728 if dc.dmConnector.group == nil || dc.dmConnector.group.epList == nil { 729 return -1 730 } 731 for i := 0; i < len(dc.dmConnector.group.epList); i++ { 732 ep := dc.dmConnector.group.epList[i] 733 if dc.dmConnector.host == ep.host && dc.dmConnector.port == ep.port { 734 return int32(i) 735 } 736 } 737 return -1 738 } 739 740 func (dc *DmConnection) getServerEncoding() string { 741 if dc.dmConnector.charCode != "" { 742 return dc.dmConnector.charCode 743 } 744 return dc.serverEncoding 745 } 746 747 func (dc *DmConnection) lobFetchAll() bool { 748 return dc.dmConnector.lobMode == 2 749 } 750 751 func (conn *DmConnection) CompatibleOracle() bool { 752 return conn.dmConnector.compatibleMode == COMPATIBLE_MODE_ORACLE 753 } 754 755 func (conn *DmConnection) CompatibleMysql() bool { 756 return conn.dmConnector.compatibleMode == COMPATIBLE_MODE_MYSQL 757 } 758 759 func (conn *DmConnection) cancel(err error) { 760 conn.canceled.Set(err) 761 fmt.Println(conn.close()) 762 } 763 764 func (conn *DmConnection) finish() { 765 if !conn.watching || conn.finished == nil { 766 return 767 } 768 select { 769 case conn.finished <- struct{}{}: 770 conn.watching = false 771 case <-conn.closech: 772 } 773 } 774 775 func (conn *DmConnection) startWatcher() { 776 watcher := make(chan context.Context, 1) 777 conn.watcher = watcher 778 finished := make(chan struct{}) 779 conn.finished = finished 780 go func() { 781 for { 782 var ctx context.Context 783 select { 784 case ctx = <-watcher: 785 case <-conn.closech: 786 return 787 } 788 789 select { 790 case <-ctx.Done(): 791 conn.cancel(ctx.Err()) 792 case <-finished: 793 case <-conn.closech: 794 return 795 } 796 } 797 }() 798 } 799 800 func (conn *DmConnection) watchCancel(ctx context.Context) error { 801 if conn.watching { 802 803 conn.cleanup() 804 return nil 805 } 806 807 if err := ctx.Err(); err != nil { 808 return err 809 } 810 811 if ctx.Done() == nil { 812 return nil 813 } 814 815 if conn.watcher == nil { 816 return nil 817 } 818 819 conn.watching = true 820 conn.watcher <- ctx 821 return nil 822 } 823 824 type noCopy struct{} 825 826 func (*noCopy) Lock() {} 827 828 type atomicBool struct { 829 _noCopy noCopy 830 value uint32 831 } 832 833 func (ab *atomicBool) IsSet() bool { 834 return atomic.LoadUint32(&ab.value) > 0 835 } 836 837 func (ab *atomicBool) Set(value bool) { 838 if value { 839 atomic.StoreUint32(&ab.value, 1) 840 } else { 841 atomic.StoreUint32(&ab.value, 0) 842 } 843 } 844 845 func (ab *atomicBool) TrySet(value bool) bool { 846 if value { 847 return atomic.SwapUint32(&ab.value, 1) == 0 848 } 849 return atomic.SwapUint32(&ab.value, 0) > 0 850 } 851 852 type atomicError struct { 853 _noCopy noCopy 854 value atomic.Value 855 } 856 857 func (ae *atomicError) Set(value error) { 858 ae.value.Store(value) 859 } 860 861 func (ae *atomicError) Value() error { 862 if v := ae.value.Load(); v != nil { 863 864 return v.(error) 865 } 866 return nil 867 }