Skip to content

Commit

Permalink
sql, tests, sqlccl: split TestExplainRedactDDL out of TestExplainRedact
Browse files Browse the repository at this point in the history
Create another version of TestExplainRedact which also tests
`EXPLAIN (REDACT)` of DDL statements. This version is in sqlccl to allow
for DDL statements using partitions.

Part of: cockroachdb#68570
Informs: cockroachdb#98746

Epic: CRDB-19756

Release note: None
  • Loading branch information
michae2 committed Mar 20, 2023
1 parent 504e7b1 commit 51a75da
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 119 deletions.
2 changes: 2 additions & 0 deletions pkg/ccl/testccl/sqlccl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_test")
go_test(
name = "sqlccl_test",
srcs = [
"explain_test.go",
"gc_job_test.go",
"main_test.go",
"run_control_test.go",
Expand Down Expand Up @@ -42,6 +43,7 @@ go_test(
"//pkg/sql/catalog/desctestutils",
"//pkg/sql/gcjob",
"//pkg/sql/isql",
"//pkg/sql/lexbase",
"//pkg/sql/sessiondatapb",
"//pkg/sql/sqlliveness/slinstance",
"//pkg/sql/sqltestutils",
Expand Down
130 changes: 130 additions & 0 deletions pkg/ccl/testccl/sqlccl/explain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2023 The Cockroach Authors.
//
// Licensed as a CockroachDB Enterprise file under the Cockroach Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt

package sqlccl

import (
"context"
gosql "database/sql"
"strings"
"testing"

"github.com/cockroachdb/cockroach/pkg/internal/sqlsmith"
"github.com/cockroachdb/cockroach/pkg/sql/lexbase"
"github.com/cockroachdb/cockroach/pkg/sql/tests"
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
"github.com/cockroachdb/cockroach/pkg/testutils/skip"
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/cockroach/pkg/util/randutil"
"github.com/cockroachdb/errors"
)

// TestExplainRedactDDL tests that variants of EXPLAIN (REDACT) do not leak
// PII. This is very similar to sql.TestExplainRedact but includes CREATE TABLE
// and ALTER TABLE statements, which could include partitioning (hence this is
// in CCL).
func TestExplainRedactDDL(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

skip.WithIssue(t, 99005)
skip.UnderStressWithIssue(t, 99005)

const numStatements = 10

ctx := context.Background()
rng, seed := randutil.NewTestRand()
t.Log("seed:", seed)

params, _ := tests.CreateTestServerParams()
s, sqlDB, _ := serverutils.StartServer(t, params)
defer s.Stopper().Stop(ctx)
defer sqlDB.Close()

query := func(sql string) (*gosql.Rows, error) {
return sqlDB.QueryContext(ctx, sql)
}

// To check for PII leaks, we inject a single unlikely string into some of the
// query constants produced by SQLSmith, and then search the redacted EXPLAIN
// output for this string.
pii := "pachycephalosaurus"
containsPII := func(sql, output string) error {
lowerOutput := strings.ToLower(output)
if strings.Contains(lowerOutput, pii) {
return errors.Newf(
"output contained PII (%q):\n%s\noutput:\n%s\n", pii, sql, output,
)
}
return nil
}

// Perform a few random initial CREATE TABLEs.
setup := sqlsmith.RandTablesPrefixStringConsts(rng, pii)
setup = append(setup, "SET CLUSTER SETTING sql.stats.automatic_collection.enabled = off;")
setup = append(setup, "SET statement_timeout = '5s';")
for _, stmt := range setup {
t.Log(stmt)
if _, err := sqlDB.ExecContext(ctx, stmt); err != nil {
// Ignore errors.
t.Log("-- ignoring error:", err)
continue
}
}

// Check EXPLAIN (OPT, CATALOG, REDACT) for each table.
rows, err := query("SELECT table_name FROM [SHOW TABLES]")
if err != nil {
t.Fatal(err)
}
var tables []string
for rows.Next() {
var table string
if err = rows.Scan(&table); err != nil {
t.Fatal(err)
}
tables = append(tables, table)
}
for _, table := range tables {
explain := "EXPLAIN (OPT, CATALOG, REDACT) SELECT * FROM " + lexbase.EscapeSQLIdent(table)
t.Log(explain)
rows, err = query(explain)
if err != nil {
// This explain should always succeed.
t.Fatal(err)
}
var output strings.Builder
for rows.Next() {
var out string
if err = rows.Scan(&out); err != nil {
t.Fatal(err)
}
output.WriteString(out)
output.WriteRune('\n')
}
if err = containsPII(explain, output.String()); err != nil {
t.Error(err)
continue
}

}

// Set up smither to generate random DDL and DML statements.
smith, err := sqlsmith.NewSmither(sqlDB, rng,
sqlsmith.PrefixStringConsts(pii),
sqlsmith.OnlySingleDMLs(),
sqlsmith.EnableAlters(),
)
if err != nil {
t.Fatal(err)
}
defer smith.Close()

tests.GenerateAndCheckRedactedExplainsForPII(t, smith, numStatements, query, containsPII)
}
45 changes: 25 additions & 20 deletions pkg/cmd/smith/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,37 @@ var (
num = flags.Int("num", 1, "number of statements / expressions to generate")
url = flags.String("url", "", "database to fetch schema from")
smitherOptMap = map[string]sqlsmith.SmitherOption{
"DisableMutations": sqlsmith.DisableMutations(),
"DisableDDLs": sqlsmith.DisableDDLs(),
"OnlyNoDropDDLs": sqlsmith.OnlyNoDropDDLs(),
"MultiRegionDDLs": sqlsmith.MultiRegionDDLs(),
"DisableWith": sqlsmith.DisableWith(),
"DisableNondeterministicFns": sqlsmith.DisableNondeterministicFns(),
"DisableCRDBFns": sqlsmith.DisableCRDBFns(),
"SimpleDatums": sqlsmith.SimpleDatums(),
"MutationsOnly": sqlsmith.MutationsOnly(),
"InsUpdOnly": sqlsmith.InsUpdOnly(),
"DisableLimits": sqlsmith.DisableLimits(),
"AvoidConsts": sqlsmith.AvoidConsts(),
"DisableWindowFuncs": sqlsmith.DisableWindowFuncs(),
"CompareMode": sqlsmith.CompareMode(),
"DisableAggregateFuncs": sqlsmith.DisableAggregateFuncs(),
"OutputSort": sqlsmith.OutputSort(),
"UnlikelyConstantPredicate": sqlsmith.UnlikelyConstantPredicate(),
"FavorCommonData": sqlsmith.FavorCommonData(),
"UnlikelyRandomNulls": sqlsmith.UnlikelyRandomNulls(),
"DisableCRDBFns": sqlsmith.DisableCRDBFns(),
"DisableCrossJoins": sqlsmith.DisableCrossJoins(),
"DisableDDLs": sqlsmith.DisableDDLs(),
"DisableDecimals": sqlsmith.DisableDecimals(),
"DisableDivision": sqlsmith.DisableDivision(),
"DisableEverything": sqlsmith.DisableEverything(),
"DisableIndexHints": sqlsmith.DisableIndexHints(),
"LowProbabilityWhereClauseWithJoinTables": sqlsmith.LowProbabilityWhereClauseWithJoinTables(),
"DisableInsertSelect": sqlsmith.DisableInsertSelect(),
"CompareMode": sqlsmith.CompareMode(),
"PostgresMode": sqlsmith.PostgresMode(),
"DisableJoins": sqlsmith.DisableJoins(),
"DisableLimits": sqlsmith.DisableLimits(),
"DisableMutations": sqlsmith.DisableMutations(),
"DisableNondeterministicFns": sqlsmith.DisableNondeterministicFns(),
"DisableWindowFuncs": sqlsmith.DisableWindowFuncs(),
"DisableWith": sqlsmith.DisableWith(),
"EnableAlters": sqlsmith.EnableAlters(),
"FavorCommonData": sqlsmith.FavorCommonData(),
"InsUpdOnly": sqlsmith.InsUpdOnly(),
"LowProbabilityWhereClauseWithJoinTables": sqlsmith.LowProbabilityWhereClauseWithJoinTables(),
"MultiRegionDDLs": sqlsmith.MultiRegionDDLs(),
"MutatingMode": sqlsmith.MutatingMode(),
"DisableDecimals": sqlsmith.DisableDecimals(),
"MutationsOnly": sqlsmith.MutationsOnly(),
"OnlyNoDropDDLs": sqlsmith.OnlyNoDropDDLs(),
"OnlySingleDMLs": sqlsmith.OnlySingleDMLs(),
"OutputSort": sqlsmith.OutputSort(),
"PostgresMode": sqlsmith.PostgresMode(),
"SimpleDatums": sqlsmith.SimpleDatums(),
"UnlikelyConstantPredicate": sqlsmith.UnlikelyConstantPredicate(),
"UnlikelyRandomNulls": sqlsmith.UnlikelyRandomNulls(),
}
smitherOpts []string
)
Expand Down
11 changes: 11 additions & 0 deletions pkg/internal/sqlsmith/sqlsmith.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,17 @@ var DisableDDLs = simpleOption("disable DDLs", func(s *Smither) {
}
})

// OnlySingleDMLs causes the Smither to only emit single-statement DML (SELECT,
// INSERT, UPDATE, DELETE).
var OnlySingleDMLs = simpleOption("only single DMLs", func(s *Smither) {
s.stmtWeights = []statementWeight{
{20, makeSelect},
{5, makeInsert},
{5, makeUpdate},
{1, makeDelete},
}
})

// OnlyNoDropDDLs causes the Smither to only emit DDLs, but won't ever drop
// a table.
var OnlyNoDropDDLs = simpleOption("only DDLs", func(s *Smither) {
Expand Down
2 changes: 0 additions & 2 deletions pkg/sql/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -872,8 +872,6 @@ go_test(
"@in_gopkg_yaml_v2//:yaml_v2",
"@org_golang_google_protobuf//proto",
"@org_golang_x_sync//errgroup",
"@org_golang_x_text//cases",
"@org_golang_x_text//language",
],
)

Expand Down
115 changes: 18 additions & 97 deletions pkg/sql/explain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package sql_test
import (
"bytes"
"context"
gosql "database/sql"
"fmt"
"regexp"
"strconv"
Expand All @@ -22,7 +23,6 @@ import (

"github.com/cockroachdb/cockroach/pkg/base"
"github.com/cockroachdb/cockroach/pkg/internal/sqlsmith"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
"github.com/cockroachdb/cockroach/pkg/sql/tests"
"github.com/cockroachdb/cockroach/pkg/testutils"
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
Expand All @@ -34,8 +34,6 @@ import (
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

func TestStatementReuses(t *testing.T) {
Expand Down Expand Up @@ -481,6 +479,8 @@ func TestExplainRedact(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

const numStatements = 10

ctx := context.Background()
rng, seed := randutil.NewTestRand()
t.Log("seed:", seed)
Expand All @@ -490,18 +490,25 @@ func TestExplainRedact(t *testing.T) {
defer s.Stopper().Stop(ctx)
defer sqlDB.Close()

query := func(sql string) (*gosql.Rows, error) {
return sqlDB.QueryContext(ctx, sql)
}

// To check for PII leaks, we inject a single unlikely string into some of the
// query constants produced by SQLSmith, and then search the EXPLAIN output
// for this string.
// query constants produced by SQLSmith, and then search the redacted EXPLAIN
// output for this string.
pii := "pterodactyl"
containsPII := func(explain, contents string) error {
lowerContents := strings.ToLower(contents)
if strings.Contains(lowerContents, pii) {
return errors.Newf("EXPLAIN output contained PII (%q):\n%s\noutput:\n%s\n", pii, explain, contents)
containsPII := func(explain, output string) error {
lowerOutput := strings.ToLower(output)
if strings.Contains(lowerOutput, pii) {
return errors.Newf(
"EXPLAIN output contained PII (%q):\n%s\noutput:\n%s\n", pii, explain, output,
)
}
return nil
}

// Set up smither to generate random DML statements.
setup := sqlsmith.Setups["seed"](rng)
setup = append(setup, "SET CLUSTER SETTING sql.stats.automatic_collection.enabled = off;")
setup = append(setup, "ANALYZE seed;")
Expand All @@ -511,99 +518,13 @@ func TestExplainRedact(t *testing.T) {
db.ExecMultiple(t, setup...)

smith, err := sqlsmith.NewSmither(sqlDB, rng,
sqlsmith.EnableAlters(),
sqlsmith.PrefixStringConsts(pii),
sqlsmith.DisableDDLs(),
)
if err != nil {
t.Fatal(err)
}
defer smith.Close()

// Generate a few random statements.
var statements [5]string
for i := range statements {
statements[i] = smith.Generate()
}

// Gather EXPLAIN variants to test.
commands := []string{"EXPLAIN", "EXPLAIN ANALYZE"}

modes := []string{"NO MODE"}
for modeStr, mode := range tree.ExplainModes() {
switch mode {
case tree.ExplainDebug:
// EXPLAIN ANALYZE (DEBUG, REDACT) is checked by TestExplainAnalyzeDebug/redact.
continue
}
modes = append(modes, modeStr)
}

flags := []string{"NO FLAG"}
for flagStr, flag := range tree.ExplainFlags() {
switch flag {
case tree.ExplainFlagRedact:
// We add REDACT to each EXPLAIN below.
continue
}
flags = append(flags, flagStr)
}

testName := func(s string) string {
return strings.ReplaceAll(cases.Title(language.English).String(s), " ", "")
}

// Execute each EXPLAIN variant on each random statement, and look for PII.
for _, cmd := range commands {
t.Run(testName(cmd), func(t *testing.T) {
for _, mode := range modes {
t.Run(testName(mode), func(t *testing.T) {
if mode == "NO MODE" {
mode = ""
} else {
mode += ", "
}
for _, flag := range flags {
t.Run(testName(flag), func(t *testing.T) {
if flag == "NO FLAG" {
flag = ""
} else {
flag += ", "
}
for _, stmt := range statements {
explain := cmd + " (" + mode + flag + "REDACT) " + stmt
rows, err := sqlDB.QueryContext(ctx, explain)
if err != nil {
// There are many legitimate errors that could be returned
// that don't indicate a PII leak or a test failure. For
// example, EXPLAIN (OPT, JSON) is always a syntax error, or
// EXPLAIN ANALYZE of a random query might timeout. To avoid
// these false positives, we only fail on internal errors.
msg := err.Error()
if strings.Contains(msg, "internal error") {
t.Error(err)
}
continue
}
var output strings.Builder
for rows.Next() {
var out string
if err := rows.Scan(&out); err != nil {
t.Fatal(err)
}
output.WriteString(out)
output.WriteRune('\n')
}
if err := containsPII(explain, output.String()); err != nil {
t.Error(err)
continue
}
// TODO(michae2): When they are supported, also check HTML returned by
// EXPLAIN (DISTSQL, REDACT) and EXPLAIN (OPT, ENV, REDACT).
}
})
}
})
}
})
}
tests.GenerateAndCheckRedactedExplainsForPII(t, smith, numStatements, query, containsPII)
}
Loading

0 comments on commit 51a75da

Please sign in to comment.