vitess.io/vitess@v0.16.2/go/vt/vttest/local_cluster.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 vttest 18 19 import ( 20 "bufio" 21 "bytes" 22 "context" 23 "encoding/json" 24 "fmt" 25 "os" 26 "os/exec" 27 "path" 28 "path/filepath" 29 "strings" 30 "time" 31 "unicode" 32 33 "vitess.io/vitess/go/vt/sidecardb" 34 35 "google.golang.org/protobuf/encoding/protojson" 36 "google.golang.org/protobuf/encoding/prototext" 37 "google.golang.org/protobuf/proto" 38 39 "vitess.io/vitess/go/mysql" 40 "vitess.io/vitess/go/sqltypes" 41 "vitess.io/vitess/go/vt/log" 42 "vitess.io/vitess/go/vt/proto/logutil" 43 "vitess.io/vitess/go/vt/vtctl/vtctlclient" 44 45 vschemapb "vitess.io/vitess/go/vt/proto/vschema" 46 vttestpb "vitess.io/vitess/go/vt/proto/vttest" 47 48 // we need to import the grpcvtctlclient library so the gRPC 49 // vtctl client is registered and can be used. 50 _ "vitess.io/vitess/go/vt/vtctl/grpcvtctlclient" 51 ) 52 53 // Config are the settings used to configure the self-contained Vitess cluster. 54 // The LocalCluster struct embeds Config so it's possible to either initialize 55 // a LocalCluster with the given settings, or set the settings directly after 56 // initialization. 57 // All settings must be set before LocalCluster.Setup() is called. 58 type Config struct { 59 // Topology defines the fake cluster's topology. This field is mandatory. 60 // See: vt/proto/vttest.VTTestTopology 61 Topology *vttestpb.VTTestTopology 62 63 // Seed can be set with a SeedConfig struct to enable 64 // auto-initialization of the database cluster with random data. 65 // If nil, no random initialization will be performed. 66 // See: SeedConfig 67 Seed *SeedConfig 68 69 // SchemaDir is the directory for schema files. Within this dir, 70 // there should be a subdir for each keyspace. Within each keyspace 71 // dir, each file is executed as SQL after the database is created on 72 // each shard. 73 // If the directory contains a `vschema.json`` file, it will be used 74 // as the VSchema for the V3 API 75 SchemaDir string 76 77 // DefaultSchemaDir is the default directory for initial schema files. 78 // If no schema is found in SchemaDir, default to this location. 79 DefaultSchemaDir string 80 81 // DataDir is the directory where the data files will be placed. 82 // If no directory is specified a random directory will be used 83 // under VTDATAROOT. 84 DataDir string 85 86 // Charset is the default charset used by MySQL 87 Charset string 88 89 // PlannerVersion is the planner version to use for the vtgate. 90 // Choose between V3, Gen4, Gen4Greedy and Gen4Fallback 91 PlannerVersion string 92 93 // ExtraMyCnf are the extra .CNF files to be added to the MySQL config 94 ExtraMyCnf []string 95 96 // OnlyMySQL can be set so only MySQL is initialized as part of the 97 // local cluster configuration. The rest of the Vitess components will 98 // not be started. 99 OnlyMySQL bool 100 101 // PersistentMode can be set so that MySQL data directory is not cleaned up 102 // when LocalCluster.TearDown() is called. This is useful for running 103 // vttestserver as a database container in local developer environments. Note 104 // that db and vschema migration files (-schema_dir option) and seeding of 105 // random data (-initialize_with_random_data option) will only run during 106 // cluster startup if the data directory does not already exist. 107 PersistentMode bool 108 109 // MySQL protocol bind address. 110 // vtcombo will bind to this address when exposing the mysql protocol socket 111 MySQLBindHost string 112 // SnapshotFile is the path to the MySQL Snapshot that will be used to 113 // initialize the mysqld instance in the cluster. Note that some environments 114 // do not suppport initialization through snapshot files. 115 SnapshotFile string 116 117 // Enable system settings to be changed per session at the database connection level 118 EnableSystemSettings bool 119 120 // TransactionMode is SINGLE, MULTI or TWOPC 121 TransactionMode string 122 123 TransactionTimeout float64 124 125 // The host name to use for the table otherwise it will be resolved from the local hostname 126 TabletHostName string 127 128 // Whether to enable/disable workflow manager 129 InitWorkflowManager bool 130 131 // Authorize vschema ddl operations to a list of users 132 VSchemaDDLAuthorizedUsers string 133 134 // How to handle foreign key constraint in CREATE/ALTER TABLE. Valid values are "allow", "disallow" 135 ForeignKeyMode string 136 137 // Allow users to submit, view, and control Online DDL 138 EnableOnlineDDL bool 139 140 // Allow users to submit direct DDL statements 141 EnableDirectDDL bool 142 143 // Allow users to start a local cluster using a remote topo server 144 ExternalTopoImplementation string 145 146 ExternalTopoGlobalServerAddress string 147 148 ExternalTopoGlobalRoot string 149 150 VtgateTabletRefreshInterval time.Duration 151 } 152 153 // InitSchemas is a shortcut for tests that just want to setup a single 154 // keyspace with a single SQL file, and/or a vschema. 155 // It creates a temporary directory, and puts the schema/vschema in there. 156 // It then sets the right value for cfg.SchemaDir. 157 // At the end of the test, the caller should os.RemoveAll(cfg.SchemaDir). 158 func (cfg *Config) InitSchemas(keyspace, schema string, vschema *vschemapb.Keyspace) error { 159 if cfg.SchemaDir != "" { 160 return fmt.Errorf("SchemaDir is already set to %v", cfg.SchemaDir) 161 } 162 163 // Create a base temporary directory. 164 tempSchemaDir, err := os.MkdirTemp("", "vttest") 165 if err != nil { 166 return err 167 } 168 169 // Write the schema if set. 170 if schema != "" { 171 ksDir := path.Join(tempSchemaDir, keyspace) 172 err = os.Mkdir(ksDir, os.ModeDir|0775) 173 if err != nil { 174 return err 175 } 176 fileName := path.Join(ksDir, "schema.sql") 177 err = os.WriteFile(fileName, []byte(schema), 0666) 178 if err != nil { 179 return err 180 } 181 } 182 183 // Write in the vschema if set. 184 if vschema != nil { 185 vschemaFilePath := path.Join(tempSchemaDir, keyspace, "vschema.json") 186 vschemaJSON, err := json.Marshal(vschema) 187 if err != nil { 188 return err 189 } 190 if err := os.WriteFile(vschemaFilePath, vschemaJSON, 0644); err != nil { 191 return err 192 } 193 } 194 cfg.SchemaDir = tempSchemaDir 195 return nil 196 } 197 198 // DbName returns the default name for a database in this cluster. 199 // If OnlyMySQL is set, this will be the name of the single database 200 // created in MySQL. Otherwise, this will be blank. 201 func (cfg *Config) DbName() string { 202 ns := cfg.Topology.GetKeyspaces() 203 if len(ns) > 0 && cfg.OnlyMySQL { 204 return ns[0].Name 205 } 206 return "" 207 } 208 209 // TopoData is a struct representing a test topology. 210 // 211 // It implements pflag.Value and can be used as a destination command-line via 212 // pflag.Var or pflag.VarP. 213 type TopoData struct { 214 vtTestTopology *vttestpb.VTTestTopology 215 unmarshal func(b []byte, m proto.Message) error 216 } 217 218 // String is part of the pflag.Value interface. 219 func (td *TopoData) String() string { 220 return prototext.Format(td.vtTestTopology) 221 } 222 223 // Set is part of the pflag.Value interface. 224 func (td *TopoData) Set(value string) error { 225 return td.unmarshal([]byte(value), td.vtTestTopology) 226 } 227 228 // Type is part of the pflag.Value interface. 229 func (td *TopoData) Type() string { return "vttest.TopoData" } 230 231 // TextTopoData returns a test TopoData that unmarshals using 232 // prototext.Unmarshal. 233 func TextTopoData(tpb *vttestpb.VTTestTopology) *TopoData { 234 return &TopoData{ 235 vtTestTopology: tpb, 236 unmarshal: prototext.Unmarshal, 237 } 238 } 239 240 // JSONTopoData returns a test TopoData that unmarshals using 241 // protojson.Unmarshal. 242 func JSONTopoData(tpb *vttestpb.VTTestTopology) *TopoData { 243 return &TopoData{ 244 vtTestTopology: tpb, 245 unmarshal: protojson.Unmarshal, 246 } 247 } 248 249 // LocalCluster controls a local Vitess setup for testing, containing 250 // a MySQL instance and one or more vtgate-equivalent access points. 251 // To use, simply create a new LocalCluster instance and either pass in 252 // the desired Config, or manually set each field on the struct itself. 253 // Once the struct is configured, call LocalCluster.Setup() to instantiate 254 // the cluster. 255 // See: Config for configuration settings on the cluster 256 type LocalCluster struct { 257 Config 258 259 // Env is the Environment which will be used for unning this local cluster. 260 // It can be set by the user before calling Setup(). If not set, Setup() will 261 // use the NewDefaultEnv callback to instantiate an environment with the system 262 // default settings 263 Env Environment 264 265 mysql MySQLManager 266 topo TopoManager 267 vt *VtProcess 268 } 269 270 // MySQLConnParams returns a mysql.ConnParams struct that can be used 271 // to connect directly to the mysqld service in the self-contained cluster 272 // This connection should be used for debug/introspection purposes; normal 273 // cluster access should be performed through the vtgate port. 274 func (db *LocalCluster) MySQLConnParams() mysql.ConnParams { 275 connParams := db.mysql.Params(db.DbName()) 276 connParams.Charset = db.Config.Charset 277 return connParams 278 } 279 280 // MySQLAppDebugConnParams returns a mysql.ConnParams struct that can be used 281 // to connect directly to the mysqld service in the self-contained cluster, 282 // using the appdebug user. It's valid only if you used MySQLOnly option. 283 func (db *LocalCluster) MySQLAppDebugConnParams() mysql.ConnParams { 284 connParams := db.MySQLConnParams() 285 connParams.Uname = "vt_appdebug" 286 return connParams 287 } 288 289 // Setup brings up the self-contained Vitess cluster by spinning up 290 // MySQL and Vitess instances. The spawned processes will be running 291 // until the TearDown() method is called. 292 // Please ensure to `defer db.TearDown()` after calling this method 293 func (db *LocalCluster) Setup() error { 294 var err error 295 296 if db.Env == nil { 297 log.Info("No environment in cluster settings. Creating default...") 298 db.Env, err = NewDefaultEnv() 299 if err != nil { 300 return err 301 } 302 } 303 304 log.Infof("LocalCluster environment: %+v", db.Env) 305 306 // Set up topo manager if we are using a remote topo server 307 if db.ExternalTopoImplementation != "" { 308 db.topo = db.Env.TopoManager(db.ExternalTopoImplementation, db.ExternalTopoGlobalServerAddress, db.ExternalTopoGlobalRoot, db.Topology) 309 log.Infof("Initializing Topo Manager: %+v", db.topo) 310 if err := db.topo.Setup(); err != nil { 311 log.Errorf("Failed to set up Topo Manager: %v", err) 312 return err 313 } 314 } 315 316 db.mysql, err = db.Env.MySQLManager(db.ExtraMyCnf, db.SnapshotFile) 317 if err != nil { 318 return err 319 } 320 321 initializing := true 322 if db.PersistentMode && dirExist(db.mysql.TabletDir()) { 323 initializing = false 324 } 325 326 if initializing { 327 log.Infof("Initializing MySQL Manager (%T)...", db.mysql) 328 if err := db.mysql.Setup(); err != nil { 329 log.Errorf("Mysqlctl failed to start: %s", err) 330 if err, ok := err.(*exec.ExitError); ok { 331 log.Errorf("stderr: %s", err.Stderr) 332 } 333 return err 334 } 335 336 if err := db.createDatabases(); err != nil { 337 return err 338 } 339 } else { 340 log.Infof("Starting MySQL Manager (%T)...", db.mysql) 341 if err := db.mysql.Start(); err != nil { 342 log.Errorf("Mysqlctl failed to start: %s", err) 343 if err, ok := err.(*exec.ExitError); ok { 344 log.Errorf("stderr: %s", err.Stderr) 345 } 346 return err 347 } 348 } 349 350 mycfg, _ := json.Marshal(db.mysql.Params("")) 351 log.Infof("MySQL up: %s", mycfg) 352 353 if !db.OnlyMySQL { 354 log.Infof("Starting vtcombo...") 355 db.vt, _ = VtcomboProcess(db.Env, &db.Config, db.mysql) 356 if err := db.vt.WaitStart(); err != nil { 357 return err 358 } 359 log.Infof("vtcombo up: %s", db.vt.Address()) 360 } 361 362 if initializing { 363 log.Info("Mysql data directory does not exist. Initializing cluster with database and vschema migrations...") 364 // Load schema will apply db and vschema migrations. Running after vtcombo starts to be able to apply vschema migrations 365 if err := db.loadSchema(true); err != nil { 366 return err 367 } 368 369 if db.Seed != nil { 370 log.Info("Populating database with random data...") 371 if err := db.populateWithRandomData(); err != nil { 372 return err 373 } 374 } 375 } else { 376 log.Info("Mysql data directory exists in persistent mode. Will only execute vschema migrations during startup") 377 if err := db.loadSchema(false); err != nil { 378 return err 379 } 380 } 381 382 return nil 383 } 384 385 // TearDown shuts down all the processes in the local cluster 386 // and cleans up any temporary on-disk data. 387 // If an error is returned, some of the running processes may not 388 // have been shut down cleanly and may need manual cleanup. 389 func (db *LocalCluster) TearDown() error { 390 var errors []string 391 392 if db.vt != nil { 393 if err := db.vt.WaitTerminate(); err != nil { 394 errors = append(errors, fmt.Sprintf("vtprocess: %s", err)) 395 } 396 } 397 398 if err := db.mysql.TearDown(); err != nil { 399 errors = append(errors, fmt.Sprintf("mysql: %s", err)) 400 401 log.Errorf("failed to shutdown MySQL: %s", err) 402 if err, ok := err.(*exec.ExitError); ok { 403 log.Errorf("stderr: %s", err.Stderr) 404 } 405 } 406 407 if !db.PersistentMode { 408 if err := db.Env.TearDown(); err != nil { 409 errors = append(errors, fmt.Sprintf("environment: %s", err)) 410 } 411 } 412 413 if len(errors) > 0 { 414 return fmt.Errorf("failed to teardown LocalCluster:\n%s", 415 strings.Join(errors, "\n")) 416 } 417 418 return nil 419 } 420 421 func (db *LocalCluster) shardNames(keyspace *vttestpb.Keyspace) (names []string) { 422 for _, spb := range keyspace.Shards { 423 dbname := spb.DbNameOverride 424 if dbname == "" { 425 dbname = fmt.Sprintf("vt_%s_%s", keyspace.Name, spb.Name) 426 } 427 names = append(names, dbname) 428 } 429 return 430 } 431 432 func isDir(path string) bool { 433 info, err := os.Stat(path) 434 return err == nil && info.IsDir() 435 } 436 437 // loadSchema applies sql and vschema migrations respectively for each keyspace in the topology 438 func (db *LocalCluster) loadSchema(shouldRunDatabaseMigrations bool) error { 439 if db.SchemaDir == "" { 440 return nil 441 } 442 443 log.Info("Loading custom schema...") 444 445 if !isDir(db.SchemaDir) { 446 return fmt.Errorf("LoadSchema(): SchemaDir does not exist") 447 } 448 449 for _, kpb := range db.Topology.Keyspaces { 450 if kpb.ServedFrom != "" { 451 // redirected keyspaces have no underlying database 452 continue 453 } 454 455 keyspace := kpb.Name 456 keyspaceDir := path.Join(db.SchemaDir, keyspace) 457 458 schemaDir := keyspaceDir 459 if !isDir(schemaDir) { 460 schemaDir = db.DefaultSchemaDir 461 if schemaDir == "" || !isDir(schemaDir) { 462 return fmt.Errorf("LoadSchema: schema dir for ks `%s` does not exist (%s)", keyspace, schemaDir) 463 } 464 } 465 466 glob, _ := filepath.Glob(path.Join(schemaDir, "*.sql")) 467 for _, filepath := range glob { 468 cmds, err := LoadSQLFile(filepath, schemaDir) 469 if err != nil { 470 return err 471 } 472 473 // One single vschema migration per file 474 if !db.OnlyMySQL && len(cmds) == 1 && strings.HasPrefix(strings.ToUpper(cmds[0]), "ALTER VSCHEMA") { 475 if err = db.applyVschema(keyspace, cmds[0]); err != nil { 476 return err 477 } 478 continue 479 } 480 481 if !shouldRunDatabaseMigrations { 482 continue 483 } 484 485 for _, dbname := range db.shardNames(kpb) { 486 if err := db.Execute(cmds, dbname); err != nil { 487 return err 488 } 489 } 490 } 491 492 if !db.OnlyMySQL { 493 if err := db.reloadSchemaKeyspace(keyspace); err != nil { 494 return err 495 } 496 } 497 } 498 499 return nil 500 } 501 502 func (db *LocalCluster) createVTSchema() error { 503 var sidecardbExec sidecardb.Exec = func(ctx context.Context, query string, maxRows int, useDB bool) (*sqltypes.Result, error) { 504 if useDB { 505 if err := db.Execute([]string{sidecardb.UseSidecarDatabaseQuery}, ""); err != nil { 506 return nil, err 507 } 508 } 509 return db.ExecuteFetch(query, "") 510 } 511 512 if err := sidecardb.Init(context.Background(), sidecardbExec); err != nil { 513 return err 514 } 515 return nil 516 } 517 func (db *LocalCluster) createDatabases() error { 518 log.Info("Creating databases in cluster...") 519 520 // The tablets created in vttest do not follow the same tablet init process, so we need to explicitly create 521 // the sidecar database tables 522 if err := db.createVTSchema(); err != nil { 523 return err 524 } 525 526 var sql []string 527 for _, kpb := range db.Topology.Keyspaces { 528 if kpb.ServedFrom != "" { 529 continue 530 } 531 for _, dbname := range db.shardNames(kpb) { 532 sql = append(sql, fmt.Sprintf("create database `%s`", dbname)) 533 } 534 } 535 return db.Execute(sql, "") 536 } 537 538 // Execute runs a series of SQL statements on the MySQL instance backing 539 // this local cluster. This is provided for debug/introspection purposes; 540 // normal cluster access should be performed through the Vitess GRPC interface. 541 func (db *LocalCluster) Execute(sql []string, dbname string) error { 542 params := db.mysql.Params(dbname) 543 conn, err := mysql.Connect(context.Background(), ¶ms) 544 if err != nil { 545 return err 546 } 547 defer conn.Close() 548 549 _, err = conn.ExecuteFetch("START TRANSACTION", 0, false) 550 if err != nil { 551 return err 552 } 553 554 for _, cmd := range sql { 555 log.Infof("Execute(%s): \"%s\"", dbname, cmd) 556 _, err := conn.ExecuteFetch(cmd, -1, false) 557 if err != nil { 558 return err 559 } 560 } 561 562 _, err = conn.ExecuteFetch("COMMIT", 0, false) 563 return err 564 } 565 566 // ExecuteFetch runs a SQL statement on the MySQL instance backing 567 // this local cluster and returns the result. 568 func (db *LocalCluster) ExecuteFetch(sql string, dbname string) (*sqltypes.Result, error) { 569 params := db.mysql.Params(dbname) 570 conn, err := mysql.Connect(context.Background(), ¶ms) 571 if err != nil { 572 return nil, err 573 } 574 defer conn.Close() 575 576 log.Infof("ExecuteFetch(%s): \"%s\"", dbname, sql) 577 rs, err := conn.ExecuteFetch(sql, -1, true) 578 return rs, err 579 } 580 581 // Query runs a SQL query on the MySQL instance backing this local cluster and returns 582 // its result. This is provided for debug/introspection purposes; 583 // normal cluster access should be performed through the Vitess GRPC interface. 584 func (db *LocalCluster) Query(sql, dbname string, limit int) (*sqltypes.Result, error) { 585 params := db.mysql.Params(dbname) 586 conn, err := mysql.Connect(context.Background(), ¶ms) 587 if err != nil { 588 return nil, err 589 } 590 defer conn.Close() 591 592 return conn.ExecuteFetch(sql, limit, false) 593 } 594 595 // JSONConfig returns a key/value object with the configuration 596 // settings for the local cluster. It should be serialized with 597 // `json.Marshal` 598 func (db *LocalCluster) JSONConfig() any { 599 if db.OnlyMySQL { 600 return db.mysql.Params("") 601 } 602 603 config := map[string]any{ 604 "port": db.vt.Port, 605 "socket": db.mysql.UnixSocket(), 606 "vtcombo_mysql_port": db.Env.PortForProtocol("vtcombo_mysql_port", ""), 607 "mysql": db.Env.PortForProtocol("mysql", ""), 608 } 609 610 if grpc := db.vt.PortGrpc; grpc != 0 { 611 config["grpc_port"] = grpc 612 } 613 614 return config 615 } 616 617 // GrpcPort returns the grpc port used by vtcombo 618 func (db *LocalCluster) GrpcPort() int { 619 return db.vt.PortGrpc 620 } 621 622 func (db *LocalCluster) applyVschema(keyspace string, migration string) error { 623 server := fmt.Sprintf("localhost:%v", db.vt.PortGrpc) 624 args := []string{"ApplyVSchema", "--sql", migration, keyspace} 625 fmt.Printf("Applying vschema %v", args) 626 err := vtctlclient.RunCommandAndWait(context.Background(), server, args, func(e *logutil.Event) { 627 log.Info(e) 628 }) 629 630 return err 631 } 632 633 func (db *LocalCluster) reloadSchemaKeyspace(keyspace string) error { 634 server := fmt.Sprintf("localhost:%v", db.vt.PortGrpc) 635 args := []string{"ReloadSchemaKeyspace", "--include_primary=true", keyspace} 636 fmt.Printf("Reloading keyspace schema %v", args) 637 638 err := vtctlclient.RunCommandAndWait(context.Background(), server, args, func(e *logutil.Event) { 639 log.Info(e) 640 }) 641 642 return err 643 } 644 645 func dirExist(dir string) bool { 646 exist := true 647 if _, err := os.Stat(dir); os.IsNotExist(err) { 648 exist = false 649 } 650 return exist 651 } 652 653 // LoadSQLFile loads a parses a .sql file from disk, removing all the 654 // different comments that mysql/mysqldump inserts in these, and returning 655 // each individual SQL statement as its own string. 656 // If sourceroot is set, that directory will be used when resolving `source ` 657 // statements in the SQL file. 658 func LoadSQLFile(filename, sourceroot string) ([]string, error) { 659 var ( 660 cmd bytes.Buffer 661 sql []string 662 inSQ bool 663 inDQ bool 664 ) 665 666 file, err := os.Open(filename) 667 if err != nil { 668 return nil, err 669 } 670 defer file.Close() 671 672 scanner := bufio.NewScanner(file) 673 for scanner.Scan() { 674 line := scanner.Text() 675 line = strings.TrimRightFunc(line, unicode.IsSpace) 676 677 if !inSQ && !inDQ && strings.HasPrefix(line, "--") { 678 continue 679 } 680 681 var i, next int 682 for { 683 i = next 684 if i >= len(line) { 685 break 686 } 687 688 next = i + 1 689 690 if line[i] == '\\' { 691 next = i + 2 692 } else if line[i] == '\'' && !inDQ { 693 inSQ = !inSQ 694 } else if line[i] == '"' && !inSQ { 695 inDQ = !inDQ 696 } else if !inSQ && !inDQ { 697 if line[i] == '#' || strings.HasPrefix(line[i:], "-- ") { 698 line = line[:i] 699 break 700 } 701 if line[i] == ';' { 702 cmd.WriteString(line[:i]) 703 sql = append(sql, cmd.String()) 704 cmd.Reset() 705 706 line = line[i+1:] 707 next = 0 708 } 709 } 710 } 711 712 if strings.TrimSpace(line) != "" { 713 if sourceroot != "" && cmd.Len() == 0 && strings.HasPrefix(line, "source ") { 714 srcfile := path.Join(sourceroot, line[7:]) 715 sql2, err := LoadSQLFile(srcfile, sourceroot) 716 if err != nil { 717 return nil, err 718 } 719 sql = append(sql, sql2...) 720 } else { 721 cmd.WriteString(line) 722 cmd.WriteByte('\n') 723 } 724 } 725 } 726 727 if cmd.Len() != 0 { 728 sql = append(sql, cmd.String()) 729 } 730 731 if err := scanner.Err(); err != nil { 732 return nil, err 733 } 734 735 return sql, nil 736 }