github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/enginetest/dolt_harness.go (about) 1 // Copyright 2020 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package enginetest 16 17 import ( 18 "context" 19 "fmt" 20 "runtime" 21 "strings" 22 "testing" 23 24 gms "github.com/dolthub/go-mysql-server" 25 "github.com/dolthub/go-mysql-server/enginetest" 26 "github.com/dolthub/go-mysql-server/enginetest/scriptgen/setup" 27 "github.com/dolthub/go-mysql-server/memory" 28 "github.com/dolthub/go-mysql-server/sql" 29 "github.com/dolthub/go-mysql-server/sql/mysql_db" 30 "github.com/dolthub/go-mysql-server/sql/rowexec" 31 "github.com/stretchr/testify/require" 32 33 "github.com/dolthub/dolt/go/libraries/doltcore/branch_control" 34 "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils" 35 "github.com/dolthub/dolt/go/libraries/doltcore/env" 36 "github.com/dolthub/dolt/go/libraries/doltcore/sqle" 37 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" 38 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/statsnoms" 39 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/statspro" 40 "github.com/dolthub/dolt/go/libraries/utils/filesys" 41 "github.com/dolthub/dolt/go/store/types" 42 ) 43 44 type DoltHarness struct { 45 t *testing.T 46 provider dsess.DoltDatabaseProvider 47 statsPro sql.StatsProvider 48 multiRepoEnv *env.MultiRepoEnv 49 session *dsess.DoltSession 50 branchControl *branch_control.Controller 51 parallelism int 52 skippedQueries []string 53 setupData []setup.SetupScript 54 resetData []setup.SetupScript 55 engine *gms.Engine 56 setupDbs map[string]struct{} 57 skipSetupCommit bool 58 configureStats bool 59 useLocalFilesystem bool 60 setupTestProcedures bool 61 } 62 63 var _ enginetest.Harness = (*DoltHarness)(nil) 64 var _ enginetest.SkippingHarness = (*DoltHarness)(nil) 65 var _ enginetest.ClientHarness = (*DoltHarness)(nil) 66 var _ enginetest.IndexHarness = (*DoltHarness)(nil) 67 var _ enginetest.VersionedDBHarness = (*DoltHarness)(nil) 68 var _ enginetest.ForeignKeyHarness = (*DoltHarness)(nil) 69 var _ enginetest.KeylessTableHarness = (*DoltHarness)(nil) 70 var _ enginetest.ReadOnlyDatabaseHarness = (*DoltHarness)(nil) 71 var _ enginetest.ValidatingHarness = (*DoltHarness)(nil) 72 73 // newDoltHarness creates a new harness for testing Dolt, using an in-memory filesystem and an in-memory blob store. 74 func newDoltHarness(t *testing.T) *DoltHarness { 75 dh := &DoltHarness{ 76 t: t, 77 skippedQueries: defaultSkippedQueries, 78 parallelism: 1, 79 } 80 81 return dh 82 } 83 84 // newDoltHarnessForLocalFilesystem creates a new harness for testing Dolt, using 85 // the local filesystem for all storage, instead of in-memory versions. This setup 86 // is useful for testing functionality that requires a real filesystem. 87 func newDoltHarnessForLocalFilesystem(t *testing.T) *DoltHarness { 88 dh := newDoltHarness(t) 89 dh.useLocalFilesystem = true 90 return dh 91 } 92 93 var defaultSkippedQueries = []string{ 94 "show variables", // we set extra variables 95 "show create table fk_tbl", // we create an extra key for the FK that vanilla gms does not 96 "show indexes from", // we create / expose extra indexes (for foreign keys) 97 "show global variables like", // we set extra variables 98 } 99 100 // Setup sets the setup scripts for this DoltHarness's engine 101 func (d *DoltHarness) Setup(setupData ...[]setup.SetupScript) { 102 d.closeProvider() 103 d.engine = nil 104 d.provider = nil 105 d.setupData = nil 106 for i := range setupData { 107 d.setupData = append(d.setupData, setupData[i]...) 108 } 109 } 110 111 func (d *DoltHarness) SkipSetupCommit() { 112 d.skipSetupCommit = true 113 } 114 115 // resetScripts returns a set of queries that will reset the given database 116 // names. If [autoInc], the queries for resetting autoincrement tables are 117 // included. 118 func (d *DoltHarness) resetScripts() []setup.SetupScript { 119 ctx := enginetest.NewContext(d) 120 _, res := enginetest.MustQuery(ctx, d.engine, "select schema_name from information_schema.schemata where schema_name not in ('information_schema');") 121 var dbs []string 122 for i := range res { 123 dbs = append(dbs, res[i][0].(string)) 124 } 125 126 var resetCmds []setup.SetupScript 127 resetCmds = append(resetCmds, setup.SetupScript{"SET foreign_key_checks=0;"}) 128 for i := range dbs { 129 db := dbs[i] 130 resetCmds = append(resetCmds, setup.SetupScript{fmt.Sprintf("use %s", db)}) 131 132 // Any auto increment tables must be dropped and recreated to get a fresh state for the global auto increment 133 // sequence trackers 134 _, aiTables := enginetest.MustQuery(ctx, d.engine, 135 fmt.Sprintf("select distinct table_name from information_schema.columns where extra = 'auto_increment' and table_schema = '%s';", db)) 136 137 for _, tableNameRow := range aiTables { 138 tableName := tableNameRow[0].(string) 139 140 // special handling for auto_increment_tbl, which is expected to start with particular values 141 if strings.ToLower(tableName) == "auto_increment_tbl" { 142 resetCmds = append(resetCmds, setup.AutoincrementData...) 143 continue 144 } 145 146 resetCmds = append(resetCmds, setup.SetupScript{fmt.Sprintf("drop table %s", tableName)}) 147 } 148 149 resetCmds = append(resetCmds, setup.SetupScript{"call dolt_clean()"}) 150 resetCmds = append(resetCmds, setup.SetupScript{"call dolt_reset('--hard', 'head')"}) 151 } 152 153 resetCmds = append(resetCmds, setup.SetupScript{"SET foreign_key_checks=1;"}) 154 for _, db := range dbs { 155 if _, ok := d.setupDbs[db]; !ok && db != "mydb" { 156 resetCmds = append(resetCmds, setup.SetupScript{fmt.Sprintf("drop database if exists %s", db)}) 157 } 158 } 159 resetCmds = append(resetCmds, setup.SetupScript{"use mydb"}) 160 return resetCmds 161 } 162 163 // commitScripts returns a set of queries that will commit the working sets of the given database names 164 func commitScripts(dbs []string) []setup.SetupScript { 165 var commitCmds setup.SetupScript 166 for i := range dbs { 167 db := dbs[i] 168 commitCmds = append(commitCmds, fmt.Sprintf("use %s", db)) 169 commitCmds = append(commitCmds, "call dolt_add('.')") 170 commitCmds = append(commitCmds, fmt.Sprintf("call dolt_commit('--allow-empty', '-am', 'checkpoint enginetest database %s', '--date', '1970-01-01T12:00:00')", db)) 171 } 172 commitCmds = append(commitCmds, "use mydb") 173 return []setup.SetupScript{commitCmds} 174 } 175 176 // NewEngine creates a new *gms.Engine or calls reset and clear scripts on the existing 177 // engine for reuse. 178 func (d *DoltHarness) NewEngine(t *testing.T) (enginetest.QueryEngine, error) { 179 initializeEngine := d.engine == nil 180 if initializeEngine { 181 d.branchControl = branch_control.CreateDefaultController(context.Background()) 182 183 pro := d.newProvider() 184 if d.setupTestProcedures { 185 pro = d.newProviderWithProcedures() 186 } 187 doltProvider, ok := pro.(*sqle.DoltDatabaseProvider) 188 require.True(t, ok) 189 d.provider = doltProvider 190 191 statsProv := statspro.NewProvider(d.provider.(*sqle.DoltDatabaseProvider), statsnoms.NewNomsStatsFactory(d.multiRepoEnv.RemoteDialProvider())) 192 d.statsPro = statsProv 193 194 var err error 195 d.session, err = dsess.NewDoltSession(enginetest.NewBaseSession(), d.provider, d.multiRepoEnv.Config(), d.branchControl, d.statsPro) 196 require.NoError(t, err) 197 198 e, err := enginetest.NewEngine(t, d, d.provider, d.setupData, d.statsPro) 199 if err != nil { 200 return nil, err 201 } 202 e.Analyzer.ExecBuilder = rowexec.DefaultBuilder 203 d.engine = e 204 205 ctx := enginetest.NewContext(d) 206 databases := pro.AllDatabases(ctx) 207 d.setupDbs = make(map[string]struct{}) 208 var dbs []string 209 for _, db := range databases { 210 dbName := db.Name() 211 dbs = append(dbs, dbName) 212 d.setupDbs[dbName] = struct{}{} 213 } 214 215 if !d.skipSetupCommit { 216 e, err = enginetest.RunSetupScripts(ctx, e, commitScripts(dbs), d.SupportsNativeIndexCreation()) 217 if err != nil { 218 return nil, err 219 } 220 } 221 222 if d.configureStats { 223 bThreads := sql.NewBackgroundThreads() 224 e = e.WithBackgroundThreads(bThreads) 225 226 dSess := dsess.DSessFromSess(ctx.Session) 227 dbCache := dSess.DatabaseCache(ctx) 228 229 dsessDbs := make([]dsess.SqlDatabase, len(dbs)) 230 for i, dbName := range dbs { 231 dsessDbs[i], _ = dbCache.GetCachedRevisionDb(fmt.Sprintf("%s/main", dbName), dbName) 232 } 233 234 ctxFact := func(context.Context) (*sql.Context, error) { 235 sess := d.newSessionWithClient(sql.Client{Address: "localhost", User: "root"}) 236 return sql.NewContext(context.Background(), sql.WithSession(sess)), nil 237 } 238 if err = statsProv.Configure(ctx, ctxFact, bThreads, dsessDbs); err != nil { 239 return nil, err 240 } 241 242 statsOnlyQueries := filterStatsOnlyQueries(d.setupData) 243 e, err = enginetest.RunSetupScripts(ctx, e, statsOnlyQueries, d.SupportsNativeIndexCreation()) 244 } 245 246 return e, nil 247 } 248 249 // Reset the mysql DB table to a clean state for this new engine 250 d.engine.Analyzer.Catalog.MySQLDb = mysql_db.CreateEmptyMySQLDb() 251 d.engine.Analyzer.Catalog.MySQLDb.AddRootAccount() 252 d.engine.Analyzer.Catalog.StatsProvider = statspro.NewProvider(d.provider.(*sqle.DoltDatabaseProvider), statsnoms.NewNomsStatsFactory(d.multiRepoEnv.RemoteDialProvider())) 253 254 // Get a fresh session if we are reusing the engine 255 if !initializeEngine { 256 var err error 257 d.session, err = dsess.NewDoltSession(enginetest.NewBaseSession(), d.provider, d.multiRepoEnv.Config(), d.branchControl, d.statsPro) 258 require.NoError(t, err) 259 } 260 261 ctx := enginetest.NewContext(d) 262 e, err := enginetest.RunSetupScripts(ctx, d.engine, d.resetScripts(), d.SupportsNativeIndexCreation()) 263 264 return e, err 265 } 266 267 func filterStatsOnlyQueries(scripts []setup.SetupScript) []setup.SetupScript { 268 var ret []string 269 for i := range scripts { 270 for _, s := range scripts[i] { 271 if strings.HasPrefix(s, "analyze table") { 272 ret = append(ret, s) 273 } 274 } 275 } 276 return []setup.SetupScript{ret} 277 } 278 279 // WithParallelism returns a copy of the harness with parallelism set to the given number of threads. A value of 0 or 280 // less means to use the system parallelism settings. 281 func (d *DoltHarness) WithParallelism(parallelism int) *DoltHarness { 282 nd := *d 283 nd.parallelism = parallelism 284 return &nd 285 } 286 287 // WithSkippedQueries returns a copy of the harness with the given queries skipped 288 func (d *DoltHarness) WithSkippedQueries(queries []string) *DoltHarness { 289 nd := *d 290 nd.skippedQueries = append(d.skippedQueries, queries...) 291 return &nd 292 } 293 294 // SkipQueryTest returns whether to skip a query 295 func (d *DoltHarness) SkipQueryTest(query string) bool { 296 lowerQuery := strings.ToLower(query) 297 for _, skipped := range d.skippedQueries { 298 if strings.Contains(lowerQuery, strings.ToLower(skipped)) { 299 return true 300 } 301 } 302 303 return false 304 } 305 306 func (d *DoltHarness) Parallelism() int { 307 if d.parallelism <= 0 { 308 309 // always test with some parallelism 310 parallelism := runtime.NumCPU() 311 312 if parallelism <= 1 { 313 parallelism = 2 314 } 315 316 return parallelism 317 } 318 319 return d.parallelism 320 } 321 322 func (d *DoltHarness) NewContext() *sql.Context { 323 return sql.NewContext(context.Background(), sql.WithSession(d.session)) 324 } 325 326 func (d *DoltHarness) NewContextWithClient(client sql.Client) *sql.Context { 327 return sql.NewContext(context.Background(), sql.WithSession(d.newSessionWithClient(client))) 328 } 329 330 func (d *DoltHarness) NewSession() *sql.Context { 331 d.session = d.newSessionWithClient(sql.Client{Address: "localhost", User: "root"}) 332 return d.NewContext() 333 } 334 335 func (d *DoltHarness) newSessionWithClient(client sql.Client) *dsess.DoltSession { 336 localConfig := d.multiRepoEnv.Config() 337 pro := d.session.Provider() 338 339 dSession, err := dsess.NewDoltSession(sql.NewBaseSessionWithClientServer("address", client, 1), pro.(dsess.DoltDatabaseProvider), localConfig, d.branchControl, d.statsPro) 340 dSession.SetCurrentDatabase("mydb") 341 require.NoError(d.t, err) 342 return dSession 343 } 344 345 func (d *DoltHarness) SupportsNativeIndexCreation() bool { 346 return true 347 } 348 349 func (d *DoltHarness) SupportsForeignKeys() bool { 350 return true 351 } 352 353 func (d *DoltHarness) SupportsKeylessTables() bool { 354 return true 355 } 356 357 func (d *DoltHarness) NewDatabases(names ...string) []sql.Database { 358 d.closeProvider() 359 d.engine = nil 360 d.provider = nil 361 362 d.branchControl = branch_control.CreateDefaultController(context.Background()) 363 364 pro := d.newProvider() 365 doltProvider, ok := pro.(*sqle.DoltDatabaseProvider) 366 require.True(d.t, ok) 367 d.provider = doltProvider 368 d.statsPro = statspro.NewProvider(doltProvider, statsnoms.NewNomsStatsFactory(d.multiRepoEnv.RemoteDialProvider())) 369 370 var err error 371 d.session, err = dsess.NewDoltSession(enginetest.NewBaseSession(), doltProvider, d.multiRepoEnv.Config(), d.branchControl, d.statsPro) 372 require.NoError(d.t, err) 373 374 // TODO: the engine tests should do this for us 375 d.session.SetCurrentDatabase("mydb") 376 377 e := enginetest.NewEngineWithProvider(d.t, d, d.provider) 378 require.NoError(d.t, err) 379 d.engine = e 380 381 for _, name := range names { 382 err := d.provider.CreateDatabase(enginetest.NewContext(d), name) 383 require.NoError(d.t, err) 384 } 385 386 ctx := enginetest.NewContext(d) 387 databases := pro.AllDatabases(ctx) 388 389 // It's important that we return the databases in the same order as the names argument 390 var dbs []sql.Database 391 for _, name := range names { 392 for _, db := range databases { 393 if db.Name() == name { 394 dbs = append(dbs, db) 395 break 396 } 397 } 398 } 399 400 return dbs 401 } 402 403 func (d *DoltHarness) NewReadOnlyEngine(provider sql.DatabaseProvider) (enginetest.QueryEngine, error) { 404 ddp, ok := provider.(*sqle.DoltDatabaseProvider) 405 if !ok { 406 return nil, fmt.Errorf("expected a DoltDatabaseProvider") 407 } 408 409 allDatabases := ddp.AllDatabases(d.NewContext()) 410 dbs := make([]dsess.SqlDatabase, len(allDatabases)) 411 locations := make([]filesys.Filesys, len(allDatabases)) 412 413 for i, db := range allDatabases { 414 dbs[i] = sqle.ReadOnlyDatabase{Database: db.(sqle.Database)} 415 loc, err := ddp.FileSystemForDatabase(db.Name()) 416 if err != nil { 417 return nil, err 418 } 419 420 locations[i] = loc 421 } 422 423 readOnlyProvider, err := sqle.NewDoltDatabaseProviderWithDatabases("main", ddp.FileSystem(), dbs, locations) 424 if err != nil { 425 return nil, err 426 } 427 428 // reset the session as well since we have swapped out the database provider, which invalidates caching assumptions 429 d.session, err = dsess.NewDoltSession(enginetest.NewBaseSession(), readOnlyProvider, d.multiRepoEnv.Config(), d.branchControl, d.statsPro) 430 require.NoError(d.t, err) 431 432 return enginetest.NewEngineWithProvider(nil, d, readOnlyProvider), nil 433 } 434 435 func (d *DoltHarness) NewDatabaseProvider() sql.MutableDatabaseProvider { 436 return d.provider 437 } 438 439 func (d *DoltHarness) Close() { 440 d.closeProvider() 441 } 442 443 func (d *DoltHarness) closeProvider() { 444 if d.provider != nil { 445 dbs := d.provider.AllDatabases(sql.NewEmptyContext()) 446 for _, db := range dbs { 447 require.NoError(d.t, db.(dsess.SqlDatabase).DbData().Ddb.Close()) 448 } 449 } 450 } 451 452 func (d *DoltHarness) newProvider() sql.MutableDatabaseProvider { 453 d.closeProvider() 454 455 var dEnv *env.DoltEnv 456 if d.useLocalFilesystem { 457 dEnv = dtestutils.CreateTestEnvForLocalFilesystem() 458 } else { 459 dEnv = dtestutils.CreateTestEnv() 460 } 461 defer dEnv.DoltDB.Close() 462 463 store := dEnv.DoltDB.ValueReadWriter().(*types.ValueStore) 464 store.SetValidateContentAddresses(true) 465 466 mrEnv, err := env.MultiEnvForDirectory(context.Background(), dEnv.Config.WriteableConfig(), dEnv.FS, dEnv.Version, dEnv) 467 require.NoError(d.t, err) 468 d.multiRepoEnv = mrEnv 469 470 b := env.GetDefaultInitBranch(d.multiRepoEnv.Config()) 471 pro, err := sqle.NewDoltDatabaseProvider(b, d.multiRepoEnv.FileSystem()) 472 require.NoError(d.t, err) 473 474 return pro 475 } 476 477 func (d *DoltHarness) newProviderWithProcedures() sql.MutableDatabaseProvider { 478 pro := d.newProvider() 479 provider, ok := pro.(*sqle.DoltDatabaseProvider) 480 require.True(d.t, ok) 481 for _, esp := range memory.ExternalStoredProcedures { 482 provider.Register(esp) 483 } 484 return provider 485 } 486 487 func (d *DoltHarness) newTable(db sql.Database, name string, schema sql.PrimaryKeySchema) (sql.Table, error) { 488 tc := db.(sql.TableCreator) 489 490 ctx := enginetest.NewContext(d) 491 ctx.Session.SetCurrentDatabase(db.Name()) 492 err := tc.CreateTable(ctx, name, schema, sql.Collation_Default, "") 493 if err != nil { 494 return nil, err 495 } 496 497 ctx = enginetest.NewContext(d) 498 ctx.Session.SetCurrentDatabase(db.Name()) 499 table, ok, err := db.GetTableInsensitive(ctx, name) 500 require.NoError(d.t, err) 501 require.True(d.t, ok, "table %s not found after creation", name) 502 return table, nil 503 } 504 505 // NewTableAsOf implements enginetest.VersionedHarness 506 // Dolt doesn't version tables per se, just the entire database. So ignore the name and schema and just create a new 507 // branch with the given name. 508 func (d *DoltHarness) NewTableAsOf(db sql.VersionedDatabase, name string, schema sql.PrimaryKeySchema, asOf interface{}) sql.Table { 509 table, err := d.newTable(db, name, schema) 510 if err != nil { 511 require.True(d.t, sql.ErrTableAlreadyExists.Is(err)) 512 } 513 514 table, ok, err := db.GetTableInsensitive(enginetest.NewContext(d), name) 515 require.NoError(d.t, err) 516 require.True(d.t, ok) 517 518 return table 519 } 520 521 // SnapshotTable implements enginetest.VersionedHarness 522 // Dolt doesn't version tables per se, just the entire database. So ignore the name and schema and just create a new 523 // branch with the given name. 524 func (d *DoltHarness) SnapshotTable(db sql.VersionedDatabase, tableName string, asOf interface{}) error { 525 e := enginetest.NewEngineWithProvider(d.t, d, d.NewDatabaseProvider()) 526 527 asOfString, ok := asOf.(string) 528 require.True(d.t, ok) 529 530 ctx := enginetest.NewContext(d) 531 532 _, iter, err := e.Query(ctx, 533 "CALL DOLT_COMMIT('-Am', 'test commit');") 534 require.NoError(d.t, err) 535 _, err = sql.RowIterToRows(ctx, iter) 536 require.NoError(d.t, err) 537 538 // Create a new branch at this commit with the given identifier 539 ctx = enginetest.NewContext(d) 540 query := "CALL dolt_branch('" + asOfString + "')" 541 542 _, iter, err = e.Query(ctx, 543 query) 544 require.NoError(d.t, err) 545 _, err = sql.RowIterToRows(ctx, iter) 546 require.NoError(d.t, err) 547 548 return nil 549 } 550 551 func (d *DoltHarness) ValidateEngine(ctx *sql.Context, e *gms.Engine) (err error) { 552 for _, db := range e.Analyzer.Catalog.AllDatabases(ctx) { 553 if err = ValidateDatabase(ctx, db); err != nil { 554 return err 555 } 556 } 557 return 558 }