From ef941381c96f9d29d7e3f33a94d13d623889ba94 Mon Sep 17 00:00:00 2001 From: David Dansby <39511285+codeaucafe@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:01:05 -0800 Subject: [PATCH] fix(analyzer): validate database exists before CREATE SCHEMA Add ValidateCreateSchema analyzer rule that checks if the current database has a valid root before allowing CREATE SCHEMA to proceed. This ensures PostgreSQL-compliant behavior where schemas must be created within an existing database. Adds Go integration test and bats end-to-end tests to verify the following: - Connection to non-existent database fails with error - CREATE SCHEMA succeeds on valid database Refs: #1863 --- server/analyzer/init.go | 2 + server/analyzer/validate_create_schema.go | 54 +++++++++++ testing/bats/doltgres.bats | 30 +++++- testing/go/create_schema_no_db_test.go | 107 ++++++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 server/analyzer/validate_create_schema.go create mode 100644 testing/go/create_schema_no_db_test.go diff --git a/server/analyzer/init.go b/server/analyzer/init.go index 34f7171d21..1047bb2457 100644 --- a/server/analyzer/init.go +++ b/server/analyzer/init.go @@ -46,6 +46,7 @@ const ( ruleId_OptimizeFunctions // optimizeFunctions ruleId_ValidateColumnDefaults // validateColumnDefaults ruleId_ValidateCreateTable // validateCreateTable + ruleId_ValidateCreateSchema // validateCreateSchema ruleId_ResolveAlterColumn // resolveAlterColumn ruleId_ValidateCreateFunction ) @@ -62,6 +63,7 @@ func Init() { analyzer.Rule{Id: ruleId_AssignUpdateCasts, Apply: AssignUpdateCasts}, analyzer.Rule{Id: ruleId_AssignTriggers, Apply: AssignTriggers}, analyzer.Rule{Id: ruleId_ValidateCreateFunction, Apply: ValidateCreateFunction}, + analyzer.Rule{Id: ruleId_ValidateCreateSchema, Apply: ValidateCreateSchema}, ) analyzer.OnceBeforeDefault = append([]analyzer.Rule{ diff --git a/server/analyzer/validate_create_schema.go b/server/analyzer/validate_create_schema.go new file mode 100644 index 0000000000..ff93621729 --- /dev/null +++ b/server/analyzer/validate_create_schema.go @@ -0,0 +1,54 @@ +// Copyright 2025 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 analyzer + +import ( + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/analyzer" + "github.com/dolthub/go-mysql-server/sql/plan" + "github.com/dolthub/go-mysql-server/sql/transform" + + "github.com/dolthub/doltgresql/core" +) + +// ValidateCreateSchema validates that CREATE SCHEMA is executed with a valid database context. +// In PostgreSQL, schemas exist within databases, so CREATE SCHEMA requires an active database. +// See: https://github.com/dolthub/doltgresql/issues/1863 +func ValidateCreateSchema( + ctx *sql.Context, + a *analyzer.Analyzer, + n sql.Node, + scope *plan.Scope, + sel analyzer.RuleSelector, + qFlags *sql.QueryFlags, +) (sql.Node, transform.TreeIdentity, error) { + cs, ok := n.(*plan.CreateSchema) + if !ok { + return n, transform.SameTree, nil + } + + // Check if the current database actually has a working root. + // GetRootFromContext will return sql.ErrDatabaseNotFound if the database + // is not properly initialized. + _, _, err := core.GetRootFromContext(ctx) + if err != nil { + if sql.ErrDatabaseNotFound.Is(err) { + return nil, transform.SameTree, sql.ErrNoDatabaseSelected.New() + } + return nil, transform.SameTree, err + } + + return cs, transform.SameTree, nil +} diff --git a/testing/bats/doltgres.bats b/testing/bats/doltgres.bats index 680f4bc616..79fbd8e2ad 100644 --- a/testing/bats/doltgres.bats +++ b/testing/bats/doltgres.bats @@ -396,7 +396,35 @@ query_server_for_user_and_pass() { shift shift shift - + nativevar PGPASSWORD "$pass" /w psql -U "$user" -h localhost -p $PORT "$@" $db } + +# Test for https://github.com/dolthub/doltgresql/issues/1863 +@test 'doltgres: connection to non-existent database fails' { + start_sql_server + + # Connecting to the default postgres database should work + run query_server -c "SELECT 1" + [ "$status" -eq 0 ] + + # Connecting to a non-existent database should fail + nativevar PGPASSWORD "password" /w + run psql -U postgres -h localhost -p $PORT -c "SELECT 1" nonexistent_db + [ "$status" -ne 0 ] + [[ "$output" =~ "does not exist" ]] || false +} + +@test 'doltgres: CREATE SCHEMA works on valid database' { + start_sql_server + + # CREATE SCHEMA should work on a valid database + run query_server -c "CREATE SCHEMA test_schema_bats" + [ "$status" -eq 0 ] + + # Verify schema was created + run query_server -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'test_schema_bats'" + [ "$status" -eq 0 ] + [[ "$output" =~ "test_schema_bats" ]] || false +} diff --git a/testing/go/create_schema_no_db_test.go b/testing/go/create_schema_no_db_test.go new file mode 100644 index 0000000000..acf4947847 --- /dev/null +++ b/testing/go/create_schema_no_db_test.go @@ -0,0 +1,107 @@ +// Copyright 2025 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 _go + +import ( + "context" + "fmt" + "testing" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dserver "github.com/dolthub/doltgresql/server" + "github.com/dolthub/doltgresql/servercfg" + "github.com/dolthub/doltgresql/servercfg/cfgdetails" +) + +// TestCreateSchemaWithNonExistentDatabase tests that connecting to a non-existent +// database fails with the appropriate error at the connection level. +// This is the PostgreSQL-compliant behavior where database validation happens +// during connection establishment. +// +// NOTE: This test cannot use RunScripts because RunScripts auto-creates the +// database before running assertions. We need to test the connection-level +// failure when connecting to a non-existent database. +// +// See: https://github.com/dolthub/doltgresql/issues/1863 +func TestCreateSchemaWithNonExistentDatabase(t *testing.T) { + port, err := sql.GetEmptyPort() + require.NoError(t, err) + + // Start server using the same pattern as CreateServerWithPort + controller, err := dserver.RunInMemory( + &servercfg.DoltgresConfig{ + DoltgresConfig: cfgdetails.DoltgresConfig{ + ListenerConfig: &cfgdetails.DoltgresListenerConfig{ + PortNumber: &port, + HostStr: &serverHost, + }, + LogLevelStr: &testServerLogLevel, + }, + }, dserver.NewListener, + ) + require.NoError(t, err) + defer func() { + controller.Stop() + require.NoError(t, controller.WaitForStop()) + }() + + ctx := context.Background() + + t.Run( + "connection to non-existent database fails", func(t *testing.T) { + // Attempt to connect to a database that doesn't exist + connStr := fmt.Sprintf( + "postgres://postgres:password@%s:%d/nonexistent_db?sslmode=disable", + serverHost, + port, + ) + _, err := pgx.Connect(ctx, connStr) + + require.Error(t, err, "connection should fail when database doesn't exist") + assert.Contains( + t, err.Error(), "does not exist", + "expected 'does not exist' error, got: %v", err, + ) + }, + ) + + t.Run( + "connection to existing database succeeds", func(t *testing.T) { + // Verify that connecting to the default "postgres" database works + connStr := fmt.Sprintf("postgres://postgres:password@%s:%d/postgres?sslmode=disable", serverHost, port) + conn, err := pgx.Connect(ctx, connStr) + require.NoError(t, err) + defer conn.Close(ctx) + + // Verify we can create a schema on the valid database + _, err = conn.Exec(ctx, "CREATE SCHEMA test_schema_1863") + require.NoError(t, err) + + // Verify the schema was created + rows, err := conn.Query( + ctx, + "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'test_schema_1863'", + ) + require.NoError(t, err) + defer rows.Close() + + require.True(t, rows.Next(), "expected to find test_schema_1863") + }, + ) +}