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 }