diff --git a/src/include/duckdb/parser/peg/inlined_grammar.gram b/src/include/duckdb/parser/peg/inlined_grammar.gram index 7a35d6d791e9..f48e4c6a5e45 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.gram +++ b/src/include/duckdb/parser/peg/inlined_grammar.gram @@ -7,6 +7,8 @@ Statement <- DropDatabaseStatement / CreateTSDictionaryStatement / DropTSDictionaryStatement / + CreateServerStatement / + DropServerStatement / CreateRoleStatement / DropRoleStatement / CreatePolicyStatement / @@ -995,6 +997,12 @@ SeqMinOrMax <- MinValue / MaxValue MinValue <- 'MINVALUE' MaxValue <- 'MAXVALUE' +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA' 'WRAPPER' Identifier ServerOptions? +DropServerStatement <- 'DROP' 'SERVER' IfExists? ColId DropBehavior? + +ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) +ServerOption <- ColLabel StringLiteral + CreateSubscriptionStatement <- 'CREATE' 'SUBSCRIPTION' Identifier SubscriptionConnection? SubscriptionConnection <- 'CONNECTION' StringLiteral SubscriptionPublication? SubscriptionPublication <- 'PUBLICATION' List(Identifier) diff --git a/src/include/duckdb/parser/peg/inlined_grammar.hpp b/src/include/duckdb/parser/peg/inlined_grammar.hpp index 19e6c3d2cb3c..08a5b1b0428b 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.hpp +++ b/src/include/duckdb/parser/peg/inlined_grammar.hpp @@ -11,6 +11,8 @@ const char INLINED_PEG_GRAMMAR[] = { " DropDatabaseStatement /\n" " CreateTSDictionaryStatement /\n" " DropTSDictionaryStatement /\n" + " CreateServerStatement /\n" + " DropServerStatement /\n" " CreateRoleStatement /\n" " DropRoleStatement /\n" " CreatePolicyStatement /\n" @@ -932,6 +934,10 @@ const char INLINED_PEG_GRAMMAR[] = { "SeqMinOrMax <- MinValue / MaxValue\n" "MinValue <- 'MINVALUE'\n" "MaxValue <- 'MAXVALUE'\n" + "CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA' 'WRAPPER' Identifier ServerOptions?\n" + "DropServerStatement <- 'DROP' 'SERVER' IfExists? ColId DropBehavior?\n" + "ServerOptions <- 'OPTIONS' Parens(List(ServerOption))\n" + "ServerOption <- ColLabel StringLiteral\n" "CreateSubscriptionStatement <- 'CREATE' 'SUBSCRIPTION' Identifier SubscriptionConnection?\n" "SubscriptionConnection <- 'CONNECTION' StringLiteral SubscriptionPublication?\n" "SubscriptionPublication <- 'PUBLICATION' List(Identifier)\n" @@ -1673,7 +1679,8 @@ const char INLINED_PEG_GRAMMAR[] = { "OptionBool <- 'TRUE' / 'FALSE'\n" "RoleOrUser <- 'ROLE' / 'USER'\n" "# Object class for GRANT/REVOKE ... ON name. Absent defaults to TABLE.\n" - "GrantObjType <- 'TABLE' / 'SEQUENCE' / 'FUNCTION' / 'DATABASE' / 'SCHEMA' / 'TYPE' / 'DOMAIN'\n" + "GrantObjType <- ForeignServerObjType / 'TABLE' / 'SEQUENCE' / 'FUNCTION' / 'DATABASE' / 'SCHEMA' / 'TYPE' / 'DOMAIN'\n" + "ForeignServerObjType <- 'FOREIGN' 'SERVER'\n" "# PUBLIC is the pseudo-role; otherwise a normal role identifier.\n" "Grantee <- 'PUBLIC' / ColId\n" "RoleOptionList <- RoleOption+\n" diff --git a/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp b/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp index 87b5c3468f50..caab1bfa347e 100644 --- a/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp +++ b/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp @@ -456,6 +456,7 @@ class PEGTransformerFactory { void RegisterNotify(); void RegisterCreateTextSearchDictionary(); void RegisterRbac(); + void RegisterForeignServer(); void RegisterPivot(); void RegisterCreateMacro(); void RegisterDrop(); @@ -523,6 +524,12 @@ class PEGTransformerFactory { ParseResult &parse_result); static unique_ptr TransformAlterTableRowSecurityStatement(PEGTransformer &transformer, ParseResult &parse_result); + // create_server.gram — CREATE/DROP SERVER walk an OPTIONS Parens(List(...)) body that the + // generator cannot auto-extract; hand-write the entry points. + static unique_ptr TransformCreateServerStatement(PEGTransformer &transformer, + ParseResult &parse_result); + static unique_ptr TransformDropServerStatement(PEGTransformer &transformer, + ParseResult &parse_result); // comment.gram static Value TransformCommentValue(PEGTransformer &transformer, ParseResult &parse_result); diff --git a/src/parser/peg/grammar/statements/common.gram b/src/parser/peg/grammar/statements/common.gram index 9d5490683175..2c66e52840a0 100644 --- a/src/parser/peg/grammar/statements/common.gram +++ b/src/parser/peg/grammar/statements/common.gram @@ -7,6 +7,8 @@ Statement <- DropDatabaseStatement / CreateTSDictionaryStatement / DropTSDictionaryStatement / + CreateServerStatement / + DropServerStatement / CreateRoleStatement / DropRoleStatement / CreatePolicyStatement / diff --git a/src/parser/peg/grammar/statements/create_server.gram b/src/parser/peg/grammar/statements/create_server.gram new file mode 100644 index 000000000000..ba2e9bb37305 --- /dev/null +++ b/src/parser/peg/grammar/statements/create_server.gram @@ -0,0 +1,5 @@ +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA' 'WRAPPER' Identifier ServerOptions? +DropServerStatement <- 'DROP' 'SERVER' IfExists? ColId DropBehavior? + +ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) +ServerOption <- ColLabel StringLiteral diff --git a/src/parser/peg/grammar/statements/rbac.gram b/src/parser/peg/grammar/statements/rbac.gram index a05e456b345f..ffd73c4713b1 100644 --- a/src/parser/peg/grammar/statements/rbac.gram +++ b/src/parser/peg/grammar/statements/rbac.gram @@ -97,7 +97,8 @@ OptionBool <- 'TRUE' / 'FALSE' RoleOrUser <- 'ROLE' / 'USER' # Object class for GRANT/REVOKE ... ON name. Absent defaults to TABLE. -GrantObjType <- 'TABLE' / 'SEQUENCE' / 'FUNCTION' / 'DATABASE' / 'SCHEMA' / 'TYPE' / 'DOMAIN' +GrantObjType <- ForeignServerObjType / 'TABLE' / 'SEQUENCE' / 'FUNCTION' / 'DATABASE' / 'SCHEMA' / 'TYPE' / 'DOMAIN' +ForeignServerObjType <- 'FOREIGN' 'SERVER' # PUBLIC is the pseudo-role; otherwise a normal role identifier. Grantee <- 'PUBLIC' / ColId diff --git a/src/parser/peg/transformer/CMakeLists.txt b/src/parser/peg/transformer/CMakeLists.txt index f5b4f54f4140..aceee24eb1f6 100644 --- a/src/parser/peg/transformer/CMakeLists.txt +++ b/src/parser/peg/transformer/CMakeLists.txt @@ -24,6 +24,7 @@ add_library_unity( transform_create_publication.cpp transform_create_subscription.cpp transform_notify.cpp + transform_foreign_server.cpp transform_create_text_search_dictionary.cpp transform_rbac.cpp transform_create_trigger.cpp diff --git a/src/parser/peg/transformer/peg_transformer_factory.cpp b/src/parser/peg/transformer/peg_transformer_factory.cpp index 0c7aee1b5639..bab644aee6f0 100644 --- a/src/parser/peg/transformer/peg_transformer_factory.cpp +++ b/src/parser/peg/transformer/peg_transformer_factory.cpp @@ -199,6 +199,12 @@ void PEGTransformerFactory::RegisterCreateTextSearchDictionary() { REGISTER_TRANSFORM(TransformDropTSDictionaryStatement); } +void PEGTransformerFactory::RegisterForeignServer() { + // create_server.gram — the OPTIONS Parens(List(...)) body is not auto-extractable. + REGISTER_TRANSFORM(TransformCreateServerStatement); + REGISTER_TRANSFORM(TransformDropServerStatement); +} + void PEGTransformerFactory::RegisterPivot() { // PivotStatement and UnpivotStatement measure parameter usage while transforming // the source table, so their top-level wrappers remain manual. @@ -275,6 +281,7 @@ PEGTransformerFactory::PEGTransformerFactory() { RegisterNotify(); RegisterCreateTextSearchDictionary(); RegisterRbac(); + RegisterForeignServer(); RegisterPivot(); RegisterCreateMacro(); RegisterDrop(); diff --git a/src/parser/peg/transformer/transform_foreign_server.cpp b/src/parser/peg/transformer/transform_foreign_server.cpp new file mode 100644 index 000000000000..bad69bbe877a --- /dev/null +++ b/src/parser/peg/transformer/transform_foreign_server.cpp @@ -0,0 +1,87 @@ +#include "duckdb/parser/peg/transformer/peg_transformer.hpp" +#include "duckdb/parser/statement/pragma_statement.hpp" +#include "duckdb/parser/expression/constant_expression.hpp" +#include "duckdb/common/types/value.hpp" +#include "duckdb/common/exception.hpp" +#include "duckdb/common/string_util.hpp" + +namespace duckdb { + +// ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) — the optional child at `child_idx`, +// shared by CREATE SERVER and CREATE USER MAPPING. Emplaces each option into `info.named_parameters`. +static void TransformServerOptionsInto(PEGTransformer &transformer, ListParseResult &list_pr, idx_t child_idx, + PragmaInfo &info) { + auto &options_opt = list_pr.Child(child_idx); + if (!options_opt.HasResult()) { + return; + } + auto &options_list = options_opt.GetResult().Cast(); + auto &list_inside = PEGTransformerFactory::ExtractResultFromParens(options_list.Child(1)); + auto elements = PEGTransformerFactory::ExtractParseResultsFromList(list_inside); + for (auto &elem_ref : elements) { + auto &elem_pr = elem_ref.get().Cast(); + // ServerOption <- ColLabel StringLiteral. Pass raw children to + // Transform so it dispatches on each node's actual type + // (ColLabel resolves a keyword/identifier; StringLiteral a literal). + auto opt_name = transformer.Transform(elem_pr.GetChild(0)); + auto opt_value = transformer.Transform(elem_pr.GetChild(1)); + auto value_expr = make_uniq(Value(opt_value)); + auto [_, inserted] = info.named_parameters.emplace(opt_name, std::move(value_expr)); + if (!inserted) { + throw InvalidInputException("conflicting or redundant options: \"%s\" specified more than once", opt_name); + } + } +} + +// CREATE SERVER [IF NOT EXISTS] name FOREIGN DATA WRAPPER fdw OPTIONS (k 'v', ...) +// -> PRAGMA create_foreign_server('name', 'fdw', if_not_exists, k := 'v', ...) +unique_ptr PEGTransformerFactory::TransformCreateServerStatement(PEGTransformer &transformer, + ParseResult &parse_result) { + auto &list_pr = parse_result.Cast(); + // Children: + // 0: 'CREATE' + // 1: 'SERVER' + // 2: IfNotExists? + // 3: ColId (server name -- a bare identifier, PG-style) + // 4: 'FOREIGN' + // 5: 'DATA'? + // 6: 'WRAPPER' + // 7: Identifier (fdw name) + // 8: ServerOptions? + bool if_not_exists = list_pr.Child(2).HasResult(); + auto server_name = transformer.Transform(list_pr.GetChild(3)); + // The fdw name is a plain Identifier leaf (not a choice/list rule). + auto fdw_name = list_pr.Child(7).identifier; + + auto result = make_uniq(); + result->info->name = "create_foreign_server"; + result->info->parameters.push_back(make_uniq(Value(server_name))); + result->info->parameters.push_back(make_uniq(Value(fdw_name))); + result->info->parameters.push_back(make_uniq(Value::BOOLEAN(if_not_exists))); + TransformServerOptionsInto(transformer, list_pr, 8, *result->info); + return std::move(result); +} + +// DROP SERVER [IF EXISTS] name -> PRAGMA drop_foreign_server('name', missing_ok) +unique_ptr PEGTransformerFactory::TransformDropServerStatement(PEGTransformer &transformer, + ParseResult &parse_result) { + auto &list_pr = parse_result.Cast(); + // Children: + // 0: 'DROP', 1: 'SERVER' + // 2: IfExists? + // 3: ColId (server name) + // 4: DropBehavior? (CASCADE / RESTRICT; RESTRICT/absent = false) + bool missing_ok = list_pr.Child(2).HasResult(); + auto server_name = transformer.Transform(list_pr.GetChild(3)); + bool cascade = false; + transformer.TransformOptional(list_pr, 4, cascade); + + auto result = make_uniq(); + result->info->name = "drop_foreign_server"; + result->info->parameters.push_back(make_uniq(Value(server_name))); + result->info->parameters.push_back(make_uniq(Value::BOOLEAN(missing_ok))); + result->info->parameters.push_back(make_uniq(Value::BOOLEAN(cascade))); + return std::move(result); +} + +} // namespace duckdb diff --git a/src/parser/peg/transformer/transform_rbac.cpp b/src/parser/peg/transformer/transform_rbac.cpp index 1a8bf125d97f..cd929b33518d 100644 --- a/src/parser/peg/transformer/transform_rbac.cpp +++ b/src/parser/peg/transformer/transform_rbac.cpp @@ -786,12 +786,19 @@ static GrantTargetInfo TransformGrantTarget(PEGTransformer &transformer, ParseRe string objtype = "TABLE"; auto &objtype_opt = named.Child(0); if (objtype_opt.HasResult()) { - auto &kw = objtype_opt.GetResult() - .Cast() - .Child(0) - .GetResult() - .Cast(); - objtype = StringUtil::Upper(kw.keyword); + auto &chosen = + objtype_opt.GetResult().Cast().Child(0).GetResult(); + if (chosen.type == ParseResultType::KEYWORD) { + objtype = StringUtil::Upper(chosen.Cast().keyword); + } else { + // A multi-keyword object type (e.g. FOREIGN SERVER) is a rule whose + // body is a keyword sequence; join them into one space-separated word. + vector words; + for (auto &child : chosen.Cast().GetChildren()) { + words.push_back(StringUtil::Upper(child.get().Cast().keyword)); + } + objtype = StringUtil::Join(words, " "); + } } return {objtype, QualifiedTableName(transformer, named.GetChild(1))}; }