259 lines
11 KiB
Go
259 lines
11 KiB
Go
// Copyright 2026 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
|
|
driver "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils/sql_server_driver"
|
|
)
|
|
|
|
// These tests assert the behavior of an optimization where dolt CLI commands
|
|
// avoid loading potentially-large local database files when the command can
|
|
// instead be serviced by a running `dolt sql-server`.
|
|
//
|
|
// The hook for asserting that no table files are loaded is the
|
|
// DOLT_TEST_ASSERT_NO_TABLE_FILES_READ environment variable. When it is set,
|
|
// dolt panics if it ever bootstraps the chunk journal or opens a table file
|
|
// from disk. We use it to prove that, when a server is running, the CLI never
|
|
// touches the local database files.
|
|
//
|
|
// Every scenario uses a data dir root which contains multiple databases
|
|
// (db_one and db_two) as subdirectories, and exercises CLI invocations both
|
|
// from the data dir root and from within a particular database subdirectory.
|
|
// At the multi-db root there is no "current database", so the commands which
|
|
// require one (`dolt log`, `dolt remote -v`) are only exercised from the
|
|
// subdirectory; from the root we use `dolt sql -q` with database-qualified
|
|
// names.
|
|
//
|
|
// The test matrix has three top-level scenarios:
|
|
//
|
|
// 1. A dolt sql-server is running in the data dir root. The CLI is invoked
|
|
// with DOLT_TEST_ASSERT_NO_TABLE_FILES_READ=1 and must succeed, because it
|
|
// dispatches to the server without reading any table files.
|
|
// 2. No server is running, but a stale sql-server.info file is present at the
|
|
// data dir root. The CLI is invoked (executing locally) and must succeed.
|
|
// 3. No server is running, but a concurrent `dolt sql` process holds the
|
|
// write lock on db_one. The CLI is invoked (executing locally); reads
|
|
// succeed, writes to db_one fail because they cannot acquire the write
|
|
// lock, and writes to the unlocked db_two succeed.
|
|
//
|
|
// In scenarios 2 and 3 the CLI must read the local database, so we do not set
|
|
// DOLT_TEST_ASSERT_NO_TABLE_FILES_READ for those invocations.
|
|
|
|
// makePopulatedRepoStore creates a fresh repo store (the data dir root) with
|
|
// two initialized databases below it, each with a committed table, some rows,
|
|
// and a remote configured.
|
|
func makePopulatedRepoStore(t *testing.T, u driver.DoltUser) (driver.RepoStore, driver.Repo, driver.Repo) {
|
|
rs, err := u.MakeRepoStore()
|
|
require.NoError(t, err)
|
|
dbOne := makePopulatedDB(t, rs, "db_one")
|
|
dbTwo := makePopulatedDB(t, rs, "db_two")
|
|
return rs, dbOne, dbTwo
|
|
}
|
|
|
|
func makePopulatedDB(t *testing.T, rs driver.RepoStore, name string) driver.Repo {
|
|
repo, err := rs.MakeRepo(name)
|
|
require.NoError(t, err)
|
|
err = repo.DoltExec("sql", "-q", "create table vals (id int primary key auto_increment, v int);"+
|
|
"insert into vals (v) values (1), (2), (3);"+
|
|
"call dolt_commit('-Am', 'initial data')")
|
|
require.NoError(t, err)
|
|
require.NoError(t, repo.CreateRemote("origin", "file://"+filepath.Join(repo.Dir, "remote")))
|
|
return repo
|
|
}
|
|
|
|
// writeStaleInfoFile writes a stale sql-server.info file into the .dolt
|
|
// directory of the data dir root. Credentials lookup walks up from the cwd, so
|
|
// invocations from both the root and a database subdirectory will discover it.
|
|
func writeStaleInfoFile(t *testing.T, rs driver.RepoStore) {
|
|
dotDolt := filepath.Join(rs.Dir, ".dolt")
|
|
require.NoError(t, os.MkdirAll(dotDolt, 0755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dotDolt, "sql-server.info"), []byte("1:3306:this_is_not_a_real_secret"), 0600))
|
|
}
|
|
|
|
// withNoTableFilesRead sets DOLT_TEST_ASSERT_NO_TABLE_FILES_READ=1 on |cmd| so
|
|
// that the command panics if it loads any table files from disk.
|
|
func withNoTableFilesRead(cmd *exec.Cmd) *exec.Cmd {
|
|
cmd.Env = append(cmd.Env, dconfig.EnvAssertNoTableFilesRead+"=1")
|
|
return cmd
|
|
}
|
|
|
|
func assertCmdSucceeds(t *testing.T, cmd *exec.Cmd) string {
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, "expected command to succeed, output:\n%s", string(out))
|
|
return string(out)
|
|
}
|
|
|
|
func assertCmdFails(t *testing.T, cmd *exec.Cmd) string {
|
|
out, err := cmd.CombinedOutput()
|
|
require.Error(t, err, "expected command to fail, output:\n%s", string(out))
|
|
return string(out)
|
|
}
|
|
|
|
func TestNoTableFilesRead(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u, err := driver.NewDoltUser()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
u.Cleanup()
|
|
})
|
|
|
|
// Sanity check: with the assertion hook enabled and no server running, CLI
|
|
// commands which read the database must fail. This proves the hook itself
|
|
// works and the success cases below are meaningful. We check both from the
|
|
// data dir root (database-qualified read) and from a database subdirectory.
|
|
t.Run("AssertHookFailsWhenLoadingTableFiles", func(t *testing.T) {
|
|
rs, dbOne, _ := makePopulatedRepoStore(t, u)
|
|
t.Run("from data dir root", func(t *testing.T) {
|
|
out := assertCmdFails(t, withNoTableFilesRead(rs.DoltCmd("sql", "-q", "select count(*) from db_one.vals")))
|
|
require.Regexp(t, "loading table files is disabled", out)
|
|
})
|
|
t.Run("from database subdirectory", func(t *testing.T) {
|
|
out := assertCmdFails(t, withNoTableFilesRead(dbOne.DoltCmd("log")))
|
|
require.Regexp(t, "loading table files is disabled", out)
|
|
})
|
|
})
|
|
|
|
// Scenario 1: a dolt sql-server is running in the data dir root. All CLI
|
|
// invocations set the assertion hook and must succeed by dispatching to the
|
|
// server.
|
|
t.Run("WithRunningServer", func(t *testing.T) {
|
|
rs, dbOne, _ := makePopulatedRepoStore(t, u)
|
|
|
|
var ports DynamicResources
|
|
ports.global = &GlobalPorts
|
|
ports.t = t
|
|
RunServerUntilEndOfTest(t, rs, &driver.Server{
|
|
Args: []string{"--port", `{{get_port "server"}}`},
|
|
DynamicPort: "server",
|
|
}, &ports)
|
|
|
|
t.Run("from data dir root", func(t *testing.T) {
|
|
t.Run("show databases", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, withNoTableFilesRead(rs.DoltCmd("sql", "-q", "show databases")))
|
|
require.Regexp(t, "db_one", out)
|
|
require.Regexp(t, "db_two", out)
|
|
})
|
|
t.Run("sql read", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, withNoTableFilesRead(rs.DoltCmd("sql", "-q", "select count(*) from db_one.vals")))
|
|
require.Regexp(t, "3", out)
|
|
})
|
|
t.Run("sql write of db_two", func(t *testing.T) {
|
|
assertCmdSucceeds(t, withNoTableFilesRead(rs.DoltCmd("sql", "-q", "insert into db_two.vals (v) values (10)")))
|
|
})
|
|
})
|
|
t.Run("from database subdirectory", func(t *testing.T) {
|
|
t.Run("sql read", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, withNoTableFilesRead(dbOne.DoltCmd("sql", "-q", "select count(*) from vals")))
|
|
require.Regexp(t, "3", out)
|
|
})
|
|
t.Run("sql write", func(t *testing.T) {
|
|
assertCmdSucceeds(t, withNoTableFilesRead(dbOne.DoltCmd("sql", "-q", "insert into vals (v) values (11)")))
|
|
})
|
|
t.Run("remote -v", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, withNoTableFilesRead(dbOne.DoltCmd("remote", "-v")))
|
|
require.Regexp(t, "origin", out)
|
|
})
|
|
t.Run("log", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, withNoTableFilesRead(dbOne.DoltCmd("log")))
|
|
require.Regexp(t, "initial data", out)
|
|
})
|
|
})
|
|
})
|
|
|
|
// Scenario 2: no server running, but a stale sql-server.info file is
|
|
// present at the data dir root. The CLI executes locally and must work.
|
|
t.Run("WithStaleInfoFile", func(t *testing.T) {
|
|
rs, dbOne, _ := makePopulatedRepoStore(t, u)
|
|
writeStaleInfoFile(t, rs)
|
|
|
|
t.Run("from data dir root", func(t *testing.T) {
|
|
t.Run("sql read", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, rs.DoltCmd("sql", "-q", "select count(*) from db_one.vals"))
|
|
require.Regexp(t, "3", out)
|
|
})
|
|
t.Run("sql write of db_two", func(t *testing.T) {
|
|
assertCmdSucceeds(t, rs.DoltCmd("sql", "-q", "insert into db_two.vals (v) values (20)"))
|
|
})
|
|
})
|
|
t.Run("from database subdirectory", func(t *testing.T) {
|
|
t.Run("sql read", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, dbOne.DoltCmd("sql", "-q", "select count(*) from vals"))
|
|
require.Regexp(t, "3", out)
|
|
})
|
|
t.Run("sql write", func(t *testing.T) {
|
|
assertCmdSucceeds(t, dbOne.DoltCmd("sql", "-q", "insert into vals (v) values (21)"))
|
|
})
|
|
t.Run("remote -v", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, dbOne.DoltCmd("remote", "-v"))
|
|
require.Regexp(t, "origin", out)
|
|
})
|
|
t.Run("log", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, dbOne.DoltCmd("log"))
|
|
require.Regexp(t, "initial data", out)
|
|
})
|
|
})
|
|
})
|
|
|
|
// Scenario 3: no server running, but a concurrent `dolt sql` process holds
|
|
// the write lock on db_one. The CLI executes locally: reads succeed, writes
|
|
// to db_one fail because they cannot acquire the exclusive write lock, and
|
|
// writes to the unlocked db_two succeed.
|
|
t.Run("WithConcurrentDoltSql", func(t *testing.T) {
|
|
rs, dbOne, _ := makePopulatedRepoStore(t, u)
|
|
RunDoltSQLUntilEndOfTest(t, dbOne)
|
|
|
|
t.Run("from data dir root", func(t *testing.T) {
|
|
t.Run("sql read of locked db_one", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, rs.DoltCmd("sql", "-q", "select count(*) from db_one.vals"))
|
|
require.Regexp(t, "3", out)
|
|
})
|
|
t.Run("sql write of locked db_one fails", func(t *testing.T) {
|
|
out := assertCmdFails(t, rs.DoltCmd("sql", "-q", "insert into db_one.vals (v) values (30)"))
|
|
require.Regexp(t, "(?i)read only", out)
|
|
})
|
|
t.Run("sql write of unlocked db_two succeeds", func(t *testing.T) {
|
|
assertCmdSucceeds(t, rs.DoltCmd("sql", "-q", "insert into db_two.vals (v) values (31)"))
|
|
})
|
|
})
|
|
t.Run("from database subdirectory", func(t *testing.T) {
|
|
t.Run("sql read", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, dbOne.DoltCmd("sql", "-q", "select count(*) from vals"))
|
|
require.Regexp(t, "3", out)
|
|
})
|
|
t.Run("sql write fails", func(t *testing.T) {
|
|
out := assertCmdFails(t, dbOne.DoltCmd("sql", "-q", "insert into vals (v) values (32)"))
|
|
require.Regexp(t, "(?i)read only", out)
|
|
})
|
|
t.Run("remote -v", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, dbOne.DoltCmd("remote", "-v"))
|
|
require.Regexp(t, "origin", out)
|
|
})
|
|
t.Run("log", func(t *testing.T) {
|
|
out := assertCmdSucceeds(t, dbOne.DoltCmd("log"))
|
|
require.Regexp(t, "initial data", out)
|
|
})
|
|
})
|
|
})
|
|
}
|