From 8973ba2619bf465141199b7277927699a4419a9f Mon Sep 17 00:00:00 2001 From: Maximilian Hoffman Date: Mon, 21 Oct 2024 14:31:24 -0700 Subject: [PATCH] Stats purge and prune (#8451) * Stats purge and prune * [ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/update.sh * fix purge test * [ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/update.sh * more fixes * [ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/update.sh * tidy * better branch handling * race edits * try to fix raceg * try to fix race * fix lock conflict --------- Co-authored-by: max-hoffman --- .../doltcore/sqle/dprocedures/init.go | 2 + .../doltcore/sqle/dprocedures/stats_funcs.go | 29 +++ .../doltcore/sqle/enginetest/stats_queries.go | 44 ++++ .../doltcore/sqle/statsnoms/database.go | 14 +- go/libraries/doltcore/sqle/statsnoms/write.go | 6 + .../doltcore/sqle/statspro/auto_refresh.go | 2 +- .../doltcore/sqle/statspro/configure.go | 9 +- .../doltcore/sqle/statspro/interface.go | 1 + .../doltcore/sqle/statspro/stats_provider.go | 214 ++++++++++++++++-- go/store/datas/database_common.go | 4 +- go/store/datas/statistics.go | 4 +- integration-tests/bats/stats.bats | 108 +++++++++ 12 files changed, 406 insertions(+), 31 deletions(-) diff --git a/go/libraries/doltcore/sqle/dprocedures/init.go b/go/libraries/doltcore/sqle/dprocedures/init.go index 9d94e910240..1b96e1f88b0 100644 --- a/go/libraries/doltcore/sqle/dprocedures/init.go +++ b/go/libraries/doltcore/sqle/dprocedures/init.go @@ -52,6 +52,8 @@ var DoltProcedures = []sql.ExternalStoredProcedureDetails{ {Name: "dolt_stats_restart", Schema: statsFuncSchema, Function: statsFunc(statsRestart)}, {Name: "dolt_stats_stop", Schema: statsFuncSchema, Function: statsFunc(statsStop)}, {Name: "dolt_stats_status", Schema: statsFuncSchema, Function: statsFunc(statsStatus)}, + {Name: "dolt_stats_prune", Schema: statsFuncSchema, Function: statsFunc(statsPrune)}, + {Name: "dolt_stats_purge", Schema: statsFuncSchema, Function: statsFunc(statsPurge)}, } // stringSchema returns a non-nullable schema with all columns as LONGTEXT. diff --git a/go/libraries/doltcore/sqle/dprocedures/stats_funcs.go b/go/libraries/doltcore/sqle/dprocedures/stats_funcs.go index de1e93f6f14..bb94ec92152 100644 --- a/go/libraries/doltcore/sqle/dprocedures/stats_funcs.go +++ b/go/libraries/doltcore/sqle/dprocedures/stats_funcs.go @@ -51,6 +51,8 @@ type AutoRefreshStatsProvider interface { CancelRefreshThread(string) StartRefreshThread(*sql.Context, dsess.DoltDatabaseProvider, string, *env.DoltEnv, dsess.SqlDatabase) error ThreadStatus(string) string + Prune(ctx *sql.Context) error + Purge(ctx *sql.Context) error } // statsRestart tries to stop and then start a refresh thread @@ -124,3 +126,30 @@ func statsDrop(ctx *sql.Context) (interface{}, error) { } return fmt.Sprintf("deleted stats ref for %s", dbName), nil } + +// statsPrune replaces the current disk contents with only the currently +// tracked in memory statistics. +func statsPrune(ctx *sql.Context) (interface{}, error) { + dSess := dsess.DSessFromSess(ctx.Session) + pro, ok := dSess.StatsProvider().(AutoRefreshStatsProvider) + if !ok { + return nil, fmt.Errorf("stats not persisted, cannot purge") + } + if err := pro.Prune(ctx); err != nil { + return "failed to prune stats databases", err + } + return "pruned all stats databases", nil +} + +// statsPurge removes the stats database from disk +func statsPurge(ctx *sql.Context) (interface{}, error) { + dSess := dsess.DSessFromSess(ctx.Session) + pro, ok := dSess.StatsProvider().(AutoRefreshStatsProvider) + if !ok { + return nil, fmt.Errorf("stats not persisted, cannot purge") + } + if err := pro.Purge(ctx); err != nil { + return "failed to purged databases", err + } + return "purged all database stats", nil +} diff --git a/go/libraries/doltcore/sqle/enginetest/stats_queries.go b/go/libraries/doltcore/sqle/enginetest/stats_queries.go index 9f6f1935fc5..4ddee622d37 100644 --- a/go/libraries/doltcore/sqle/enginetest/stats_queries.go +++ b/go/libraries/doltcore/sqle/enginetest/stats_queries.go @@ -816,6 +816,50 @@ var StatProcTests = []queries.ScriptTest{ }, }, }, + { + Name: "test purge", + SetUpScript: []string{ + "set @@PERSIST.dolt_stats_auto_refresh_enabled = 0;", + "CREATE table xy (x bigint primary key, y int, z varchar(500), key(y,z));", + "insert into xy values (1, 1, 'a'), (2,1,'a'), (3,1,'a'), (4,2,'b'), (5,2,'b'), (6,3,'c');", + "analyze table xy", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "select count(*) as cnt from dolt_statistics group by table_name, index_name order by cnt", + Expected: []sql.Row{{1}, {1}}, + }, + { + Query: "call dolt_stats_purge()", + }, + { + Query: "select count(*) from dolt_statistics;", + Expected: []sql.Row{{0}}, + }, + }, + }, + { + Name: "test prune", + SetUpScript: []string{ + "set @@PERSIST.dolt_stats_auto_refresh_enabled = 0;", + "CREATE table xy (x bigint primary key, y int, z varchar(500), key(y,z));", + "insert into xy values (1, 1, 'a'), (2,1,'a'), (3,1,'a'), (4,2,'b'), (5,2,'b'), (6,3,'c');", + "analyze table xy", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "select count(*) as cnt from dolt_statistics group by table_name, index_name order by cnt", + Expected: []sql.Row{{1}, {1}}, + }, + { + Query: "call dolt_stats_prune()", + }, + { + Query: "select count(*) from dolt_statistics;", + Expected: []sql.Row{{2}}, + }, + }, + }, } // TestProviderReloadScriptWithEngine runs the test script given with the engine provided. diff --git a/go/libraries/doltcore/sqle/statsnoms/database.go b/go/libraries/doltcore/sqle/statsnoms/database.go index 4e7be79a33d..47b9029d467 100644 --- a/go/libraries/doltcore/sqle/statsnoms/database.go +++ b/go/libraries/doltcore/sqle/statsnoms/database.go @@ -34,6 +34,7 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/table/editor" "github.com/dolthub/dolt/go/libraries/utils/earl" "github.com/dolthub/dolt/go/libraries/utils/filesys" + "github.com/dolthub/dolt/go/store/datas" "github.com/dolthub/dolt/go/store/hash" "github.com/dolthub/dolt/go/store/prolly" "github.com/dolthub/dolt/go/store/types" @@ -132,13 +133,24 @@ func (n *NomsStatsDatabase) Close() error { return n.destDb.DbData().Ddb.Close() } +func (n *NomsStatsDatabase) Branches() []string { + return n.branches +} + func (n *NomsStatsDatabase) LoadBranchStats(ctx *sql.Context, branch string) error { statsMap, err := n.destDb.DbData().Ddb.GetStatistics(ctx, branch) if errors.Is(err, doltdb.ErrNoStatistics) { - return nil + return n.trackBranch(ctx, branch) + } else if errors.Is(err, datas.ErrNoBranchStats) { + return n.trackBranch(ctx, branch) } else if err != nil { return err } + if cnt, err := statsMap.Count(); err != nil { + return err + } else if cnt == 0 { + return n.trackBranch(ctx, branch) + } doltStats, err := loadStats(ctx, n.sourceDb, statsMap) if err != nil { return err diff --git a/go/libraries/doltcore/sqle/statsnoms/write.go b/go/libraries/doltcore/sqle/statsnoms/write.go index 7c61fcaf1ce..c23e1d93dc8 100644 --- a/go/libraries/doltcore/sqle/statsnoms/write.go +++ b/go/libraries/doltcore/sqle/statsnoms/write.go @@ -46,6 +46,9 @@ func (n *NomsStatsDatabase) replaceStats(ctx context.Context, statsMap *prolly.M } func deleteIndexRows(ctx context.Context, statsMap *prolly.MutableMap, dStats *statspro.DoltStats) error { + if ctx.Err() != nil { + return ctx.Err() + } sch := schema.StatsTableDoltSchema kd, _ := sch.GetMapDescriptors() @@ -89,6 +92,9 @@ func deleteIndexRows(ctx context.Context, statsMap *prolly.MutableMap, dStats *s } func putIndexRows(ctx context.Context, statsMap *prolly.MutableMap, dStats *statspro.DoltStats) error { + if ctx.Err() != nil { + return ctx.Err() + } sch := schema.StatsTableDoltSchema kd, vd := sch.GetMapDescriptors() diff --git a/go/libraries/doltcore/sqle/statspro/auto_refresh.go b/go/libraries/doltcore/sqle/statspro/auto_refresh.go index 7d51e9465c8..7e61b1ded1e 100644 --- a/go/libraries/doltcore/sqle/statspro/auto_refresh.go +++ b/go/libraries/doltcore/sqle/statspro/auto_refresh.go @@ -53,7 +53,7 @@ func (p *Provider) InitAutoRefreshWithParams(ctxFactory func(ctx context.Context defer p.mu.Unlock() dropDbCtx, dbStatsCancel := context.WithCancel(context.Background()) - p.cancelers[dbName] = dbStatsCancel + p.autoCtxCancelers[dbName] = dbStatsCancel return bThreads.Add(fmt.Sprintf("%s_%s", asyncAutoRefreshStats, dbName), func(ctx context.Context) { ticker := time.NewTicker(checkInterval + time.Nanosecond) diff --git a/go/libraries/doltcore/sqle/statspro/configure.go b/go/libraries/doltcore/sqle/statspro/configure.go index 13e8378ed11..dd037293f57 100644 --- a/go/libraries/doltcore/sqle/statspro/configure.go +++ b/go/libraries/doltcore/sqle/statspro/configure.go @@ -138,11 +138,14 @@ func (p *Provider) Load(ctx *sql.Context, fs filesys.Filesys, db dsess.SqlDataba } for _, branch := range branches { - err = statsDb.LoadBranchStats(ctx, branch) - if err != nil { + if err = statsDb.LoadBranchStats(ctx, branch); err != nil { // if branch name is invalid, continue loading rest // TODO: differentiate bad branch name from other errors - ctx.GetLogger().Errorf("load stats failure: %s\n", err.Error()) + ctx.GetLogger().Errorf("load stats init failure: %s\n", err.Error()) + continue + } + if err := statsDb.Flush(ctx, branch); err != nil { + ctx.GetLogger().Errorf("load stats flush failure: %s\n", err.Error()) continue } } diff --git a/go/libraries/doltcore/sqle/statspro/interface.go b/go/libraries/doltcore/sqle/statspro/interface.go index e88ef2e4054..270f57859c9 100644 --- a/go/libraries/doltcore/sqle/statspro/interface.go +++ b/go/libraries/doltcore/sqle/statspro/interface.go @@ -53,6 +53,7 @@ type Database interface { SetLatestHash(branch, tableName string, h hash.Hash) GetLatestHash(branch, tableName string) hash.Hash + Branches() []string } // StatsFactory instances construct statistic databases. diff --git a/go/libraries/doltcore/sqle/statspro/stats_provider.go b/go/libraries/doltcore/sqle/statspro/stats_provider.go index b22e0b48b6c..1e7548477f3 100644 --- a/go/libraries/doltcore/sqle/statspro/stats_provider.go +++ b/go/libraries/doltcore/sqle/statspro/stats_provider.go @@ -18,11 +18,13 @@ import ( "context" "errors" "fmt" + "path/filepath" "strings" "sync" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/dolt/go/libraries/doltcore/dbfactory" "github.com/dolthub/dolt/go/libraries/doltcore/env" "github.com/dolthub/dolt/go/libraries/doltcore/sqle" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" @@ -49,13 +51,14 @@ type updateOrdinal struct { func NewProvider(pro *sqle.DoltDatabaseProvider, sf StatsFactory) *Provider { return &Provider{ - pro: pro, - sf: sf, - mu: &sync.Mutex{}, - statDbs: make(map[string]Database), - cancelers: make(map[string]context.CancelFunc), - status: make(map[string]string), - lockedTables: make(map[string]bool), + pro: pro, + sf: sf, + mu: &sync.Mutex{}, + statDbs: make(map[string]Database), + autoCtxCancelers: make(map[string]context.CancelFunc), + analyzeCtxCancelers: make(map[string]context.CancelFunc), + status: make(map[string]string), + lockedTables: make(map[string]bool), } } @@ -63,14 +66,15 @@ func NewProvider(pro *sqle.DoltDatabaseProvider, sf StatsFactory) *Provider { // Each database has its own statistics table that all tables/indexes in a db // share. type Provider struct { - mu *sync.Mutex - pro *sqle.DoltDatabaseProvider - sf StatsFactory - statDbs map[string]Database - cancelers map[string]context.CancelFunc - starter sqle.InitDatabaseHook - status map[string]string - lockedTables map[string]bool + mu *sync.Mutex + pro *sqle.DoltDatabaseProvider + sf StatsFactory + statDbs map[string]Database + autoCtxCancelers map[string]context.CancelFunc + analyzeCtxCancelers map[string]context.CancelFunc + starter sqle.InitDatabaseHook + status map[string]string + lockedTables map[string]bool } // each database has one statistics table that is a collection of the @@ -94,6 +98,16 @@ func newDbStats(dbName string) *dbToStats { var _ sql.StatsProvider = (*Provider)(nil) +func (p *Provider) Close() error { + var lastErr error + for _, db := range p.statDbs { + if err := db.Close(); err != nil { + lastErr = err + } + } + return lastErr +} + func (p *Provider) TryLockForUpdate(branch, db, table string) bool { p.mu.Lock() defer p.mu.Unlock() @@ -130,7 +144,7 @@ func (p *Provider) SetStarter(hook sqle.InitDatabaseHook) { func (p *Provider) CancelRefreshThread(dbName string) { p.mu.Lock() - if cancel, ok := p.cancelers[dbName]; ok { + if cancel, ok := p.autoCtxCancelers[dbName]; ok { cancel() } p.mu.Unlock() @@ -243,18 +257,12 @@ func (p *Provider) GetStats(ctx *sql.Context, qual sql.StatQualifier, _ []string return stat, true } -func (p *Provider) DropDbStats(ctx *sql.Context, db string, flush bool) error { +func (p *Provider) DropDbBranchStats(ctx *sql.Context, branch, db string, flush bool) error { statDb, ok := p.getStatDb(db) if !ok { return nil } - dSess := dsess.DSessFromSess(ctx.Session) - branch, err := dSess.GetBranch() - if err != nil { - return err - } - p.mu.Lock() defer p.mu.Unlock() @@ -268,6 +276,16 @@ func (p *Provider) DropDbStats(ctx *sql.Context, db string, flush bool) error { return nil } +func (p *Provider) DropDbStats(ctx *sql.Context, db string, flush bool) error { + dSess := dsess.DSessFromSess(ctx.Session) + branch, err := dSess.GetBranch() + if err != nil { + return err + } + + return p.DropDbBranchStats(ctx, branch, db, flush) +} + func (p *Provider) DropStats(ctx *sql.Context, qual sql.StatQualifier, _ []string) error { statDb, ok := p.getStatDb(qual.Db()) if !ok { @@ -336,3 +354,153 @@ func (p *Provider) DataLength(ctx *sql.Context, db string, table sql.Table) (uin return priStats.AvgSize(), nil } + +func (p *Provider) Prune(ctx *sql.Context) error { + dSess := dsess.DSessFromSess(ctx.Session) + + for _, sqlDb := range p.pro.DoltDatabases() { + dbName := strings.ToLower(sqlDb.Name()) + sqlDb, ok, err := dSess.Provider().SessionDatabase(ctx, dbName) + if err != nil { + return err + } + if !ok { + continue + } + statDb, ok := p.getStatDb(dbName) + if !ok { + continue + } + + // Canceling refresh thread prevents background thread from + // making progress. Prune should succeed. + p.CancelRefreshThread(dbName) + + tables, err := sqlDb.GetTableNames(ctx) + if err != nil { + return err + } + + for _, branch := range statDb.Branches() { + err := func() error { + // function closure ensures safe defers + var stats []sql.Statistic + for _, t := range tables { + // XXX: avoid races with ANALYZE with the table locks. + // Either concurrent purge or analyze (or both) will fail. + if !p.TryLockForUpdate(branch, dbName, t) { + p.mu.Lock() + fmt.Println(p.lockedTables) + p.mu.Unlock() + return fmt.Errorf("concurrent statistics update and prune; retry prune when update is finished") + } + defer p.UnlockTable(branch, dbName, t) + + tableStats, err := p.GetTableDoltStats(ctx, branch, dbName, t) + if err != nil { + return err + } + stats = append(stats, tableStats...) + } + + if err := p.DropDbBranchStats(ctx, branch, dbName, true); err != nil { + return err + } + + for _, s := range stats { + ds, ok := s.(*DoltStats) + if !ok { + return fmt.Errorf("unexpected statistics type found: %T", s) + } + if err := statDb.SetStat(ctx, branch, ds.Qualifier(), ds); err != nil { + return err + } + } + if err := statDb.Flush(ctx, branch); err != nil { + return err + } + return nil + }() + if err != nil { + return err + } + } + } + return nil +} + +func (p *Provider) Purge(ctx *sql.Context) error { + for _, sqlDb := range p.pro.DoltDatabases() { + dbName := strings.ToLower(sqlDb.Name()) + + tables, err := sqlDb.GetTableNames(ctx) + if err != nil { + return err + } + + var branches []string + db, ok := p.getStatDb(dbName) + if ok { + // Canceling refresh thread prevents background thread from + // making progress. Purge should succeed. + p.CancelRefreshThread(dbName) + + branches = db.Branches() + for _, branch := range branches { + err := func() error { + for _, t := range tables { + // XXX: avoid races with ANALYZE with the table locks. + // Either concurrent purge or analyze (or both) will fail. + if !p.TryLockForUpdate(branch, dbName, t) { + return fmt.Errorf("concurrent statistics update and prune; retry purge when update is finished") + } + defer p.UnlockTable(branch, dbName, t) + } + + err := p.DropDbBranchStats(ctx, branch, dbName, true) + if err != nil { + return fmt.Errorf("failed to drop stats: %w", err) + } + return nil + }() + if err != nil { + return err + } + } + } + + // if the database's failed to load, we still want to delete the folder + + fs, err := p.pro.FileSystemForDatabase(dbName) + if err != nil { + return err + } + + //remove from filesystem + statsFs, err := fs.WithWorkingDir(dbfactory.DoltStatsDir) + if err != nil { + return err + } + + if ok, _ := statsFs.Exists(""); ok { + if err := statsFs.Delete("", true); err != nil { + return err + } + } + + dropDbLoc, err := statsFs.Abs("") + if err != nil { + return err + } + + if err = dbfactory.DeleteFromSingletonCache(filepath.ToSlash(dropDbLoc + "/.dolt/noms")); err != nil { + return err + } + if len(branches) == 0 { + // if stats db was invalid on startup, recreate from baseline + branches = p.getStatsBranches(ctx) + } + p.Load(ctx, fs, sqlDb, branches) + } + return nil +} diff --git a/go/store/datas/database_common.go b/go/store/datas/database_common.go index 48075f99a5d..a63e97521b4 100644 --- a/go/store/datas/database_common.go +++ b/go/store/datas/database_common.go @@ -99,7 +99,7 @@ func (db *database) loadDatasetsNomsMap(ctx context.Context, rootHash hash.Hash) } if val == nil { - return types.EmptyMap, fmt.Errorf("Root hash doesn't exist: %v", rootHash) + return types.EmptyMap, fmt.Errorf("root hash doesn't exist: %s", rootHash) } return val.(types.Map), nil @@ -116,7 +116,7 @@ func (db *database) loadDatasetsRefmap(ctx context.Context, rootHash hash.Hash) } if val == nil { - return prolly.AddressMap{}, fmt.Errorf("Root hash doesn't exist: %v", rootHash) + return prolly.AddressMap{}, fmt.Errorf("root hash doesn't exist: %s", rootHash) } return parse_storeroot([]byte(val.(types.SerialMessage)), db.nodeStore()) diff --git a/go/store/datas/statistics.go b/go/store/datas/statistics.go index 01ef00ca5e3..ec12c27fab3 100644 --- a/go/store/datas/statistics.go +++ b/go/store/datas/statistics.go @@ -30,6 +30,8 @@ import ( "github.com/dolthub/dolt/go/store/types" ) +var ErrNoBranchStats = errors.New("stats for branch not found") + type Statistics struct { m prolly.Map addr hash.Hash @@ -76,7 +78,7 @@ func LoadStatistics(ctx context.Context, nbf *types.NomsBinFormat, ns tree.NodeS } if val == nil { - return nil, errors.New("root hash doesn't exist") + return nil, ErrNoBranchStats } return parse_Statistics(ctx, []byte(val.(types.SerialMessage)), ns, vr) diff --git a/integration-tests/bats/stats.bats b/integration-tests/bats/stats.bats index 0d1149d5849..926ad2b7af1 100644 --- a/integration-tests/bats/stats.bats +++ b/integration-tests/bats/stats.bats @@ -224,6 +224,114 @@ EOF [ "${lines[1]}" = "4" ] } +@test "stats: dolt_state_purge cli" { + cd repo2 + + dolt sql -q "insert into xy values (0,0), (1,0), (2,0)" + + # setting variables doesn't hang or error + dolt sql -q "SET @@persist.dolt_stats_auto_refresh_enabled = 0;" + + dolt sql -q "analyze table xy" + #start_sql_server + + #sleep 1 + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] + + dolt sql -q "call dolt_stats_purge()" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "0" ] +} + +@test "stats: dolt_state_purge server" { + cd repo2 + + dolt sql -q "insert into xy values (0,0), (1,0), (2,0)" + + # setting variables doesn't hang or error + dolt sql -q "SET @@persist.dolt_stats_auto_refresh_enabled = 0;" + + start_sql_server + + sleep 1 + + dolt sql -q "analyze table xy" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] + + dolt sql -q "call dolt_stats_purge()" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "0" ] + + dolt sql -q "analyze table xy" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] + + stop_sql_server +} + +@test "stats: dolt_state_prune cli" { + cd repo2 + + dolt sql -q "insert into xy values (0,0), (1,0), (2,0)" + + # setting variables doesn't hang or error + dolt sql -q "SET @@persist.dolt_stats_auto_refresh_enabled = 0;" + + dolt sql -q "analyze table xy" + #start_sql_server + + #sleep 1 + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] + + dolt sql -q "call dolt_stats_prune()" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] +} + +@test "stats: dolt_state_prune server" { + cd repo2 + + dolt sql -q "insert into xy values (0,0), (1,0), (2,0)" + + # setting variables doesn't hang or error + dolt sql -q "SET @@persist.dolt_stats_auto_refresh_enabled = 0;" + + start_sql_server + + sleep 1 + + dolt sql -q "analyze table xy" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] + + dolt sql -q "call dolt_stats_prune()" + + run dolt sql -r csv -q "select count(*) from dolt_statistics" + [ "$status" -eq 0 ] + [ "${lines[1]}" = "2" ] + + stop_sql_server +} + @test "stats: add/delete table" { cd repo1