diff --git a/docs/generated/sql/bnf/create_database_stmt.bnf b/docs/generated/sql/bnf/create_database_stmt.bnf index 04131090fb5d..44b0319c9e53 100644 --- a/docs/generated/sql/bnf/create_database_stmt.bnf +++ b/docs/generated/sql/bnf/create_database_stmt.bnf @@ -1,3 +1,3 @@ create_database_stmt ::= - 'CREATE' 'DATABASE' database_name ( 'WITH' | ) opt_template_clause ( 'ENCODING' ( '=' | ) encoding | ) opt_lc_collate_clause opt_lc_ctype_clause ( 'CONNECTION' 'LIMIT' ( '=' | ) limit | ) ( ( 'PRIMARY' 'REGION' ( '=' | ) region_name ) | ) ( ( 'REGIONS' ) ( '=' | ) region_name_list | ) ( ( 'SURVIVE' ( '=' | ) 'REGION' 'FAILURE' | 'SURVIVE' ( '=' | ) 'ZONE' 'FAILURE' ) | ) + 'CREATE' 'DATABASE' database_name ( 'WITH' | ) opt_template_clause ( 'ENCODING' ( '=' | ) encoding | ) opt_lc_collate_clause opt_lc_ctype_clause ( 'CONNECTION' 'LIMIT' ( '=' | ) limit | ) ( ( 'PRIMARY' 'REGION' ( '=' | ) region_name ) | ) ( ( 'REGIONS' ) ( '=' | ) region_name_list | ) ( ( 'SURVIVE' ( '=' | ) 'REGION' 'FAILURE' | 'SURVIVE' ( '=' | ) 'ZONE' 'FAILURE' ) | ) opt_owner_clause | 'CREATE' 'DATABASE' 'IF' 'NOT' 'EXISTS' database_name ( 'WITH' | ) opt_template_clause ( 'ENCODING' ( '=' | ) encoding | ) opt_lc_collate_clause opt_lc_ctype_clause ( 'CONNECTION' 'LIMIT' ( '=' | ) limit | ) ( ( 'PRIMARY' 'REGION' ( '=' | ) region_name ) | ) ( ( 'REGIONS' ) ( '=' | ) region_name_list | ) ( ( 'SURVIVE' ( '=' | ) 'REGION' 'FAILURE' | 'SURVIVE' ( '=' | ) 'ZONE' 'FAILURE' ) | ) diff --git a/docs/generated/sql/bnf/stmt_block.bnf b/docs/generated/sql/bnf/stmt_block.bnf index 3dbc25a06120..2d677efee1d7 100644 --- a/docs/generated/sql/bnf/stmt_block.bnf +++ b/docs/generated/sql/bnf/stmt_block.bnf @@ -1409,7 +1409,7 @@ for_schedules_clause ::= | 'FOR' 'SCHEDULE' a_expr create_database_stmt ::= - 'CREATE' 'DATABASE' database_name opt_with opt_template_clause opt_encoding_clause opt_lc_collate_clause opt_lc_ctype_clause opt_connection_limit opt_primary_region_clause opt_regions_list opt_survival_goal_clause + 'CREATE' 'DATABASE' database_name opt_with opt_template_clause opt_encoding_clause opt_lc_collate_clause opt_lc_ctype_clause opt_connection_limit opt_primary_region_clause opt_regions_list opt_survival_goal_clause opt_owner_clause | 'CREATE' 'DATABASE' 'IF' 'NOT' 'EXISTS' database_name opt_with opt_template_clause opt_encoding_clause opt_lc_collate_clause opt_lc_ctype_clause opt_connection_limit opt_primary_region_clause opt_regions_list opt_survival_goal_clause create_index_stmt ::= @@ -1953,6 +1953,10 @@ opt_survival_goal_clause ::= survival_goal_clause | +opt_owner_clause ::= + 'OWNER' opt_equal role_spec + | + opt_unique ::= 'UNIQUE' | diff --git a/pkg/sql/descriptor.go b/pkg/sql/descriptor.go index 0fa75216f7d6..a2402322727e 100644 --- a/pkg/sql/descriptor.go +++ b/pkg/sql/descriptor.go @@ -129,14 +129,26 @@ func (p *planner) createDatabase( return nil, false, err } + owner := p.SessionData().User() + if !database.Owner.Undefined() { + owner, err = database.Owner.ToSQLUsername(p.SessionData(), security.UsernameValidation) + if err != nil { + return nil, true, err + } + } + desc := dbdesc.NewInitial( id, string(database.Name), - p.SessionData().User(), + owner, dbdesc.MaybeWithDatabaseRegionConfig(regionConfig), dbdesc.WithPublicSchemaID(publicSchemaID), ) + if err := p.checkCanAlterToNewOwner(ctx, desc, owner); err != nil { + return nil, true, err + } + if err := p.createDescriptorWithID(ctx, dKey, id, desc, nil, jobDesc); err != nil { return nil, true, err } @@ -144,6 +156,7 @@ func (p *planner) createDatabase( // Initialize the multi-region database by creating the multi-region enum and // database-level zone configuration if there is a region config on the // descriptor. + if err := p.maybeInitializeMultiRegionDatabase(ctx, desc, regionConfig); err != nil { return nil, true, err } diff --git a/pkg/sql/logictest/testdata/logic_test/database b/pkg/sql/logictest/testdata/logic_test/database index 89772d8173dc..52c46f7f1939 100644 --- a/pkg/sql/logictest/testdata/logic_test/database +++ b/pkg/sql/logictest/testdata/logic_test/database @@ -274,3 +274,31 @@ CREATE TABLE db69713.s.pg_constraintdef_test ( statement ok DROP DATABASE db69713; + +# Ensure user must exist to create with owner. +statement error role/user "fake_user" does not exist +CREATE DATABASE aa with owner fake_user + +statement ok +CREATE DATABASE a with owner testuser + +query TTTTT colnames +SHOW DATABASES +---- +database_name owner primary_region regions survival_goal +a testuser NULL {} NULL +b root NULL {} NULL +c root NULL {} NULL +defaultdb root NULL {} NULL +postgres root NULL {} NULL +system node NULL {} NULL +test root NULL {} NULL + +# Non-superusers also must be a member of the new owning role. +statement ok +CREATE USER testuser2 + +user testuser + +statement error permission denied to create database +CREATE DATABASE d WITH OWNER testuser2 diff --git a/pkg/sql/parser/sql.y b/pkg/sql/parser/sql.y index a25268d18d12..3f1c975256e4 100644 --- a/pkg/sql/parser/sql.y +++ b/pkg/sql/parser/sql.y @@ -1331,7 +1331,7 @@ func (u *sqlSymUnion) setVar() *tree.SetVar { %type unrestricted_name type_function_name type_function_name_no_crdb_extra %type non_reserved_word %type non_reserved_word_or_sconst -%type role_spec +%type role_spec opt_owner_clause %type role_spec_list %type zone_value %type string_or_placeholder @@ -7433,7 +7433,7 @@ sequence_option_list: | sequence_option_list sequence_option_elem { $$.val = append($1.seqOpts(), $2.seqOpt()) } sequence_option_elem: - AS typename { + AS typename { // Valid option values must be integer types (ex. int2, bigint) parsedType := $2.colType() if parsedType.Family() != types.IntFamily { @@ -8758,7 +8758,7 @@ transaction_deferrable_mode: // %Text: CREATE DATABASE [IF NOT EXISTS] // %SeeAlso: WEBDOCS/create-database.html create_database_stmt: - CREATE DATABASE database_name opt_with opt_template_clause opt_encoding_clause opt_lc_collate_clause opt_lc_ctype_clause opt_connection_limit opt_primary_region_clause opt_regions_list opt_survival_goal_clause opt_placement_clause + CREATE DATABASE database_name opt_with opt_template_clause opt_encoding_clause opt_lc_collate_clause opt_lc_ctype_clause opt_connection_limit opt_primary_region_clause opt_regions_list opt_survival_goal_clause opt_placement_clause opt_owner_clause { $$.val = &tree.CreateDatabase{ Name: tree.Name($3), @@ -8771,6 +8771,7 @@ create_database_stmt: Regions: $11.nameList(), SurvivalGoal: $12.survivalGoal(), Placement: $13.dataPlacement(), + Owner: $14.roleSpec(), } } | CREATE DATABASE IF NOT EXISTS database_name opt_with opt_template_clause opt_encoding_clause opt_lc_collate_clause opt_lc_ctype_clause opt_connection_limit opt_primary_region_clause opt_regions_list opt_survival_goal_clause opt_placement_clause @@ -8916,6 +8917,18 @@ opt_connection_limit: $$.val = int32(-1) } +opt_owner_clause: + OWNER opt_equal role_spec + { + $$ = $3 + } +| /* EMPTY */ + { + $$.val = tree.RoleSpec{ + RoleSpecType: tree.CurrentUser, + } + } + opt_equal: '=' {} | /* EMPTY */ {} diff --git a/pkg/sql/sem/tree/create.go b/pkg/sql/sem/tree/create.go index 53640a553510..7d732a745765 100644 --- a/pkg/sql/sem/tree/create.go +++ b/pkg/sql/sem/tree/create.go @@ -49,6 +49,7 @@ type CreateDatabase struct { Regions NameList SurvivalGoal SurvivalGoal Placement DataPlacement + Owner RoleSpec } // Format implements the NodeFormatter interface. @@ -115,6 +116,11 @@ func (node *CreateDatabase) Format(ctx *FmtCtx) { ctx.WriteString(" ") ctx.FormatNode(&node.Placement) } + + if node.Owner.Name != "" { + ctx.WriteString(" OWNER = ") + ctx.FormatNode(&node.Owner) + } } // IndexElem represents a column with a direction in a CREATE INDEX statement.