github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/importccl/mysql_testdata_helpers_test.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  package importccl
    10  
    11  import (
    12  	"bufio"
    13  	gosql "database/sql"
    14  	"math/rand"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"testing"
    19  	"time"
    20  	"unicode/utf8"
    21  
    22  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    23  	"github.com/cockroachdb/cockroach/pkg/util/envutil"
    24  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    25  	_ "github.com/go-sql-driver/mysql"
    26  )
    27  
    28  // This can be toggled to re-write the `testdata`. Requires local mysql
    29  // configured with no password access for `root`, a db called `test` and
    30  // `OUTFILE` enabled. On OSX, this can be setup using `brew install mysql` and
    31  // the following ~/.my.cnf:
    32  // [mysqld_safe]
    33  // [mysqld]
    34  // secure_file_priv=""
    35  var rewriteMysqlTestData = envutil.EnvOrDefaultBool("COCKROACH_REWRITE_MYSQL_TESTDATA", false)
    36  
    37  const mysqlTestDB = "cockroachtestdata"
    38  
    39  const injectNull = "inject-null"
    40  
    41  func randStr(r *rand.Rand, from []rune, length int) string {
    42  	s := make([]rune, length)
    43  	for i := 0; i < length; i++ {
    44  		s[i] = from[r.Intn(len(from))]
    45  	}
    46  	return string(s)
    47  }
    48  
    49  type simpleTestRow struct {
    50  	i int
    51  	s string
    52  	b []byte
    53  }
    54  
    55  var simpleTestRows = func() []simpleTestRow {
    56  	badChars := []rune{'a', ';', '\n', ',', '"', '\\', '\r', '<', '\t', '✅', 'π', rune(0), rune(10), rune(2425), rune(5183), utf8.RuneError}
    57  	r := rand.New(rand.NewSource(1))
    58  	testRows := []simpleTestRow{
    59  		{i: 1, s: `str`},
    60  		{i: 2, s: ``},
    61  		{i: 3, s: ` `},
    62  		{i: 4, s: `,`},
    63  		{i: 5, s: "\n"},
    64  		{i: 6, s: `\n`},
    65  		{i: 7, s: "\r\n"},
    66  		{i: 8, s: "\r"},
    67  		{i: 9, s: `"`},
    68  
    69  		{i: 10, s: injectNull},
    70  		{i: 11, s: `\N`},
    71  		{i: 12, s: `NULL`},
    72  
    73  		// Unicode
    74  		{i: 13, s: `¢`},
    75  		{i: 14, s: ` ¢ `},
    76  		{i: 15, s: `✅`},
    77  		{i: 16, s: `","\n,™¢`},
    78  		{i: 17, s: string([]rune{rune(0)})},
    79  		{i: 18, s: `✅¢©ƒƒƒƒåß∂√œ∫∑∆πœ∑˚¬≤µµç∫ø∆œ∑∆¬œ∫œ∑´´†¥¨ˆˆπ‘“æ…¬…¬˚ß∆å˚˙ƒ∆©˙©∂˙≥≤Ω˜˜µ√∫∫Ω¥∑`},
    80  		{i: 19, s: `a quote " or two quotes "" and a quote-comma ", , and then a quote and newline "` + "\n"},
    81  		{i: 20, s: `"a slash \, a double slash \\, a slash+quote \",  \` + "\n"},
    82  	}
    83  
    84  	for i := 0; i < 10; i++ {
    85  		buf := make([]byte, 200)
    86  		r.Seed(int64(i))
    87  		r.Read(buf)
    88  		testRows = append(testRows, simpleTestRow{i: len(testRows) + 1, s: randStr(r, badChars, 1000), b: buf})
    89  	}
    90  	return testRows
    91  }()
    92  
    93  type everythingTestRow struct {
    94  	i   int
    95  	e   string
    96  	c   string
    97  	bin []byte
    98  	dt  time.Time
    99  	iz  int
   100  	iw  int
   101  	fl  float64
   102  	d53 string
   103  	j   string
   104  }
   105  
   106  var everythingTestRows = func() []everythingTestRow {
   107  	return []everythingTestRow{
   108  		{1, "Small", "c", []byte("bin"), timeutil.Unix(946684800, 0), 1, -2, -1.5, "-12.345", `{"a": "b", "c": {"d": ["e", 11, null]}}`},
   109  		{2, "Large", "c2", []byte("bin2"), timeutil.Unix(946684800, 0), 3525343334, 3, 1.2, "12.345", `{}`},
   110  	}
   111  }()
   112  
   113  type testFiles struct {
   114  	simple, second, everything, wholeDB string
   115  }
   116  
   117  func getMysqldumpTestdata(t *testing.T) testFiles {
   118  	var files testFiles
   119  
   120  	files.simple = filepath.Join(`testdata`, `mysqldump`, `simple.sql`)
   121  	files.second = filepath.Join(`testdata`, `mysqldump`, `second.sql`)
   122  	files.everything = filepath.Join(`testdata`, `mysqldump`, `everything.sql`)
   123  	files.wholeDB = filepath.Join(`testdata`, `mysqldump`, `db.sql`)
   124  
   125  	if rewriteMysqlTestData {
   126  		genMysqlTestdata(t, func() {
   127  			mysqldump(t, files.simple, "simple")
   128  			mysqldump(t, files.second, "second")
   129  			mysqldump(t, files.everything, "everything")
   130  			mysqldump(t, files.wholeDB, "")
   131  		})
   132  
   133  		_ = os.Remove(files.wholeDB + ".bz2")
   134  		out, err := exec.Command("bzip2", "-k", files.wholeDB).CombinedOutput()
   135  		if err != nil {
   136  			t.Fatal(err, string(out))
   137  		}
   138  		gzipFile(t, files.wholeDB)
   139  	}
   140  	return files
   141  }
   142  
   143  type outfileDumpCfg struct {
   144  	name     string
   145  	filename string
   146  	opts     roachpb.MySQLOutfileOptions
   147  	null     string
   148  }
   149  
   150  func getMysqlOutfileTestdata(t *testing.T) ([]simpleTestRow, []outfileDumpCfg) {
   151  	configs := []outfileDumpCfg{
   152  		{
   153  			name: "escape-and-enclose",
   154  			opts: roachpb.MySQLOutfileOptions{
   155  				FieldSeparator: '\t',
   156  				RowSeparator:   '\n',
   157  				HasEscape:      true,
   158  				Escape:         '\\',
   159  				Enclose:        roachpb.MySQLOutfileOptions_Always,
   160  				Encloser:       '"',
   161  			},
   162  			null: `\N`,
   163  		},
   164  		{
   165  			name: "csv-ish",
   166  			opts: roachpb.MySQLOutfileOptions{
   167  				FieldSeparator: ',',
   168  				RowSeparator:   '\n',
   169  				HasEscape:      true,
   170  				Escape:         '\\',
   171  				Enclose:        roachpb.MySQLOutfileOptions_Always,
   172  				Encloser:       '"',
   173  			},
   174  			null: `\N`,
   175  		},
   176  		{
   177  			name: "escape-quote-csv-no-enclose",
   178  			opts: roachpb.MySQLOutfileOptions{
   179  				FieldSeparator: ',',
   180  				RowSeparator:   '\n',
   181  				HasEscape:      true,
   182  				Escape:         '"',
   183  			},
   184  			null: `"N`,
   185  		},
   186  	}
   187  
   188  	for i := range configs {
   189  		configs[i].filename = filepath.Join(`testdata`, `mysqlout`, configs[i].name, `simple.txt`)
   190  	}
   191  
   192  	if rewriteMysqlTestData {
   193  		genMysqlTestdata(t, func() {
   194  			if err := os.RemoveAll(filepath.Join(`testdata`, `mysqlout`)); err != nil {
   195  				t.Fatal(err)
   196  			}
   197  			for _, cfg := range configs {
   198  				dest := filepath.Dir(cfg.filename)
   199  				if err := os.MkdirAll(dest, 0777); err != nil {
   200  					t.Fatal(err)
   201  				}
   202  
   203  				flags := []string{`-u`, `root`, mysqlTestDB, `simple`, `--tab`, `./` + dest}
   204  				if cfg.opts.Enclose == roachpb.MySQLOutfileOptions_Always {
   205  					flags = append(flags, "--fields-enclosed-by", string(cfg.opts.Encloser))
   206  				}
   207  				if cfg.opts.HasEscape {
   208  					flags = append(flags, "--fields-escaped-by", string(cfg.opts.Escape))
   209  				}
   210  				if cfg.opts.FieldSeparator != '\t' {
   211  					flags = append(flags, "--fields-terminated-by", string(cfg.opts.FieldSeparator))
   212  				}
   213  				if cfg.opts.RowSeparator != '\n' {
   214  					flags = append(flags, "--lines-terminated-by", string(cfg.opts.RowSeparator))
   215  				}
   216  				if res, err := exec.Command(
   217  					`mysqldump`, flags...,
   218  				).CombinedOutput(); err != nil {
   219  					t.Fatal(err, string(res))
   220  				}
   221  
   222  				if err := os.Remove(filepath.Join(dest, "simple.sql")); err != nil {
   223  					t.Fatal(err)
   224  				}
   225  			}
   226  		})
   227  	}
   228  
   229  	return simpleTestRows, configs
   230  }
   231  
   232  const secondTableRows = 7
   233  
   234  // genMysqlTestdata connects to the a local mysql, creates tables and testdata,
   235  // calls the dump() func and then cleans up.
   236  func genMysqlTestdata(t *testing.T, dump func()) {
   237  	db, err := gosql.Open("mysql", "root@/"+mysqlTestDB)
   238  	if err != nil {
   239  		t.Fatal(err)
   240  	}
   241  	defer db.Close()
   242  
   243  	dropTables := `DROP TABLE IF EXISTS everything, third, second, simple CASCADE`
   244  	if _, err := db.Exec(dropTables); err != nil {
   245  		t.Fatal(err)
   246  	}
   247  
   248  	for _, schema := range []string{
   249  		`CREATE TABLE simple (i INT PRIMARY KEY AUTO_INCREMENT, s text, b binary(200))`,
   250  		`CREATE TABLE SECOND (
   251  			i INT PRIMARY KEY,
   252  			k INT,
   253  			FOREIGN KEY (k) REFERENCES simple (i) ON UPDATE CASCADE,
   254  			UNIQUE KEY ik (i, k),
   255  			KEY ki (k, i)
   256  		)`,
   257  		`CREATE TABLE third (
   258  			i INT PRIMARY KEY AUTO_INCREMENT,
   259  			a INT, b INT, C INT,
   260  			FOREIGN KEY (a, b) REFERENCES second (i, k) ON DELETE RESTRICT ON UPDATE RESTRICT,
   261  			FOREIGN KEY (c) REFERENCES third (i) ON UPDATE CASCADE
   262  		)`,
   263  		`CREATE TABLE everything (
   264  				i INT PRIMARY KEY,
   265  
   266  				c       CHAR(10) NOT NULL,
   267  				s       VARCHAR(100) DEFAULT 'this is s\'s default value',
   268  				tx      TEXT,
   269  				e       ENUM('Small', 'Medium', 'Large'),
   270  
   271  				bin     BINARY(100) NOT NULL,
   272  				vbin    VARBINARY(100),
   273  				bl      BLOB,
   274  
   275  				dt      DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00',
   276  				d       DATE,
   277  				ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   278  				t       TIME,
   279  				-- TODO(dt): fix parser: for YEAR's length option
   280  				-- y       YEAR,
   281  
   282  				de      DECIMAL,
   283  				nu      NUMERIC,
   284  				d53     DECIMAL(5,3),
   285  
   286  				iw      INT(5) NOT NULL,
   287  				iz      INT ZEROFILL,
   288  				ti      TINYINT DEFAULT 5,
   289  				si      SMALLINT,
   290  				mi      MEDIUMINT,
   291  				bi      BIGINT,
   292  
   293  				fl      FLOAT NOT NULL,
   294  				rl      REAL,
   295  				db      DOUBLE,
   296  
   297  				f17     FLOAT(17),
   298  				f47     FLOAT(47),
   299  				f75     FLOAT(7, 5),
   300  				j       JSON
   301  		)`,
   302  	} {
   303  		if _, err := db.Exec(schema); err != nil {
   304  			t.Fatal(err)
   305  		}
   306  	}
   307  
   308  	for _, tc := range simpleTestRows {
   309  		s := &tc.s
   310  		if *s == injectNull {
   311  			s = nil
   312  		}
   313  		if _, err := db.Exec(
   314  			`INSERT INTO simple (s, b) VALUES (?, ?)`, s, tc.b,
   315  		); err != nil {
   316  			t.Fatal(err)
   317  		}
   318  	}
   319  	for i := 1; i <= secondTableRows; i++ {
   320  		if _, err := db.Exec(`INSERT INTO second VALUES (?, ?)`, -i, i); err != nil {
   321  			t.Fatal(err)
   322  		}
   323  	}
   324  
   325  	for _, r := range everythingTestRows {
   326  		if _, err := db.Exec(
   327  			`INSERT INTO everything (
   328  			i, e, c, bin, dt, iz, iw, fl, d53, j
   329  		) VALUES (
   330  			?, ?, ?, ?, ?, ?, ?, ?, ?, ?
   331  		)`, r.i, r.e, r.c, r.bin, r.dt, r.iz, r.iw, r.fl, r.d53, r.j); err != nil {
   332  			t.Fatal(err)
   333  		}
   334  	}
   335  
   336  	dump()
   337  
   338  	if _, err := db.Exec(dropTables); err != nil {
   339  		t.Fatal(err)
   340  	}
   341  }
   342  
   343  func mysqldump(t *testing.T, dest string, table string) {
   344  	if err := os.MkdirAll(filepath.Dir(dest), 0777); err != nil {
   345  		t.Fatal(err)
   346  	}
   347  	out, err := os.Create(dest)
   348  	if err != nil {
   349  		t.Fatal(err)
   350  	}
   351  	defer out.Close()
   352  	writer := bufio.NewWriter(out)
   353  
   354  	args := []string{`-u`, `root`, `cockroachtestdata`}
   355  	if table != "" {
   356  		args = append(args, table)
   357  	}
   358  	cmd := exec.Command(`mysqldump`, args...)
   359  	cmd.Stdout = writer
   360  	cmd.Stderr = os.Stderr
   361  	if err := cmd.Start(); err != nil {
   362  		t.Fatal(err)
   363  	}
   364  	if err := cmd.Wait(); err != nil {
   365  		t.Fatal(err)
   366  	}
   367  	if err := writer.Flush(); err != nil {
   368  		t.Fatal(err)
   369  	}
   370  	if err := out.Sync(); err != nil {
   371  		t.Fatal(err)
   372  	}
   373  }