From ba16edc2d2521580850c3b9fe9e9b5b72da415a3 Mon Sep 17 00:00:00 2001 From: Pavel && Konstantin Vedernikoffs Date: Thu, 25 Jun 2026 00:15:47 +0000 Subject: [PATCH 1/6] WIP: PEG grammar / parser changes for FDW CREATE SERVER --- .../extensions/clickhouse_scanner.cmake | 6 + .../duckdb/parser/peg/inlined_grammar.gram | 13 ++ .../duckdb/parser/peg/inlined_grammar.hpp | 11 ++ .../peg/transformer/peg_transformer.hpp | 14 ++ src/parser/peg/grammar/statements/common.gram | 4 + .../peg/grammar/statements/create_server.gram | 5 + .../statements/create_user_mapping.gram | 3 + src/parser/peg/transformer/CMakeLists.txt | 2 + .../transformer/peg_transformer_factory.cpp | 14 ++ .../transformer/transform_create_server.cpp | 103 +++++++++++++ .../transform_create_user_mapping.cpp | 144 ++++++++++++++++++ 11 files changed, 319 insertions(+) create mode 100644 .github/config/extensions/clickhouse_scanner.cmake create mode 100644 src/parser/peg/grammar/statements/create_server.gram create mode 100644 src/parser/peg/grammar/statements/create_user_mapping.gram create mode 100644 src/parser/peg/transformer/transform_create_server.cpp create mode 100644 src/parser/peg/transformer/transform_create_user_mapping.cpp diff --git a/.github/config/extensions/clickhouse_scanner.cmake b/.github/config/extensions/clickhouse_scanner.cmake new file mode 100644 index 000000000000..025623836776 --- /dev/null +++ b/.github/config/extensions/clickhouse_scanner.cmake @@ -0,0 +1,6 @@ +if (NOT MINGW AND NOT ${WASM_ENABLED}) + duckdb_extension_load(clickhouse_scanner + SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/duckdb_clickhouse + INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/duckdb_clickhouse/src/include + ) +endif() diff --git a/src/include/duckdb/parser/peg/inlined_grammar.gram b/src/include/duckdb/parser/peg/inlined_grammar.gram index 7a35d6d791e9..28dd12af214b 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.gram +++ b/src/include/duckdb/parser/peg/inlined_grammar.gram @@ -7,6 +7,10 @@ Statement <- DropDatabaseStatement / CreateTSDictionaryStatement / DropTSDictionaryStatement / + CreateServerStatement / + DropServerStatement / + CreateUserMappingStatement / + DropUserMappingStatement / CreateRoleStatement / DropRoleStatement / CreatePolicyStatement / @@ -994,6 +998,11 @@ SeqOwnedBy <- 'OWNED' 'BY' QualifiedName SeqMinOrMax <- MinValue / MaxValue MinValue <- 'MINVALUE' MaxValue <- 'MAXVALUE' +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? QualifiedName 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? +DropServerStatement <- 'DROP' 'SERVER' IfExists? QualifiedName DropBehavior? + +ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) +ServerOption <- ColLabel StringLiteral CreateSubscriptionStatement <- 'CREATE' 'SUBSCRIPTION' Identifier SubscriptionConnection? SubscriptionConnection <- 'CONNECTION' StringLiteral SubscriptionPublication? @@ -1137,6 +1146,10 @@ CreateTypeFromType <- Type EnumSelectType <- 'ENUM' Parens(SelectStatementInternal) EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?) +CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' QualifiedName ServerOptions? +DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' QualifiedName +UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel + CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal CreateRecursive <- 'RECURSIVE' diff --git a/src/include/duckdb/parser/peg/inlined_grammar.hpp b/src/include/duckdb/parser/peg/inlined_grammar.hpp index 19e6c3d2cb3c..43fed8f61684 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.hpp +++ b/src/include/duckdb/parser/peg/inlined_grammar.hpp @@ -11,6 +11,10 @@ const char INLINED_PEG_GRAMMAR[] = { " DropDatabaseStatement /\n" " CreateTSDictionaryStatement /\n" " DropTSDictionaryStatement /\n" + " CreateServerStatement /\n" + " DropServerStatement /\n" + " CreateUserMappingStatement /\n" + " DropUserMappingStatement /\n" " CreateRoleStatement /\n" " DropRoleStatement /\n" " CreatePolicyStatement /\n" @@ -932,6 +936,10 @@ const char INLINED_PEG_GRAMMAR[] = { "SeqMinOrMax <- MinValue / MaxValue\n" "MinValue <- 'MINVALUE'\n" "MaxValue <- 'MAXVALUE'\n" + "CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? QualifiedName 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions?\n" + "DropServerStatement <- 'DROP' 'SERVER' IfExists? QualifiedName 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" @@ -1052,6 +1060,9 @@ const char INLINED_PEG_GRAMMAR[] = { "CreateTypeFromType <- Type\n" "EnumSelectType <- 'ENUM' Parens(SelectStatementInternal)\n" "EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?)\n" + "CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' QualifiedName ServerOptions?\n" + "DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' QualifiedName\n" + "UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel\n" "CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal\n" "CreateRecursive <- 'RECURSIVE'\n" "DeallocateStatement <- 'DEALLOCATE' DeallocatePrepare? DeallocateTarget\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..6ecdd295c0e9 100644 --- a/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp +++ b/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp @@ -456,6 +456,8 @@ class PEGTransformerFactory { void RegisterNotify(); void RegisterCreateTextSearchDictionary(); void RegisterRbac(); + void RegisterCreateServer(); + void RegisterCreateUserMapping(); void RegisterPivot(); void RegisterCreateMacro(); void RegisterDrop(); @@ -523,6 +525,18 @@ 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); + // create_user_mapping.gram — CREATE/DROP USER MAPPING reuse the OPTIONS Parens(List(...)) body and a + // PUBLIC/CURRENT_USER/USER/role choice that the generator cannot auto-extract; hand-write the entry points. + static unique_ptr TransformCreateUserMappingStatement(PEGTransformer &transformer, + ParseResult &parse_result); + static unique_ptr TransformDropUserMappingStatement(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..7d0c622749a6 100644 --- a/src/parser/peg/grammar/statements/common.gram +++ b/src/parser/peg/grammar/statements/common.gram @@ -7,6 +7,10 @@ Statement <- DropDatabaseStatement / CreateTSDictionaryStatement / DropTSDictionaryStatement / + CreateServerStatement / + DropServerStatement / + CreateUserMappingStatement / + DropUserMappingStatement / 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..360b20729980 --- /dev/null +++ b/src/parser/peg/grammar/statements/create_server.gram @@ -0,0 +1,5 @@ +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? QualifiedName 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? +DropServerStatement <- 'DROP' 'SERVER' IfExists? QualifiedName DropBehavior? + +ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) +ServerOption <- ColLabel StringLiteral diff --git a/src/parser/peg/grammar/statements/create_user_mapping.gram b/src/parser/peg/grammar/statements/create_user_mapping.gram new file mode 100644 index 000000000000..586b5e7e1ed3 --- /dev/null +++ b/src/parser/peg/grammar/statements/create_user_mapping.gram @@ -0,0 +1,3 @@ +CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' QualifiedName ServerOptions? +DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' QualifiedName +UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel diff --git a/src/parser/peg/transformer/CMakeLists.txt b/src/parser/peg/transformer/CMakeLists.txt index f5b4f54f4140..d918f98c6f40 100644 --- a/src/parser/peg/transformer/CMakeLists.txt +++ b/src/parser/peg/transformer/CMakeLists.txt @@ -24,6 +24,8 @@ add_library_unity( transform_create_publication.cpp transform_create_subscription.cpp transform_notify.cpp + transform_create_server.cpp + transform_create_user_mapping.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..f33bcebd1801 100644 --- a/src/parser/peg/transformer/peg_transformer_factory.cpp +++ b/src/parser/peg/transformer/peg_transformer_factory.cpp @@ -199,6 +199,18 @@ void PEGTransformerFactory::RegisterCreateTextSearchDictionary() { REGISTER_TRANSFORM(TransformDropTSDictionaryStatement); } +void PEGTransformerFactory::RegisterCreateServer() { + // create_server.gram — OPTIONS Parens(List(...)) body is not auto-extractable. + REGISTER_TRANSFORM(TransformCreateServerStatement); + REGISTER_TRANSFORM(TransformDropServerStatement); +} + +void PEGTransformerFactory::RegisterCreateUserMapping() { + // create_user_mapping.gram — OPTIONS Parens(List(...)) body and the role choice are not auto-extractable. + REGISTER_TRANSFORM(TransformCreateUserMappingStatement); + REGISTER_TRANSFORM(TransformDropUserMappingStatement); +} + void PEGTransformerFactory::RegisterPivot() { // PivotStatement and UnpivotStatement measure parameter usage while transforming // the source table, so their top-level wrappers remain manual. @@ -275,6 +287,8 @@ PEGTransformerFactory::PEGTransformerFactory() { RegisterNotify(); RegisterCreateTextSearchDictionary(); RegisterRbac(); + RegisterCreateServer(); + RegisterCreateUserMapping(); RegisterPivot(); RegisterCreateMacro(); RegisterDrop(); diff --git a/src/parser/peg/transformer/transform_create_server.cpp b/src/parser/peg/transformer/transform_create_server.cpp new file mode 100644 index 000000000000..b5e3552d514a --- /dev/null +++ b/src/parser/peg/transformer/transform_create_server.cpp @@ -0,0 +1,103 @@ +#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" + +namespace duckdb { + +// Convert a QualifiedName back into a dotted name. Mirrors the helper used by +// transform_create_text_search_dictionary.cpp. +static string ServerQualifiedNameToDottedString(const QualifiedName &name) { + string result; + if (!name.Catalog().empty()) { + result += name.Catalog(); + result += "."; + } + if (!name.Schema().empty()) { + result += name.Schema(); + result += "."; + } + result += name.Name(); + return result; +} + +// 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: QualifiedName + // 4: 'FOREIGN' + // 5: 'DATA'? + // 6: 'WRAPPER' + // 7: Identifier (fdw name) + // 8: ServerOptions? + bool if_not_exists = list_pr.Child(2).HasResult(); + auto qname = transformer.Transform(list_pr.Child(3)); + auto server_name = ServerQualifiedNameToDottedString(qname); + // 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))); + + // ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) + auto &options_opt = list_pr.Child(8); + if (options_opt.HasResult()) { + auto &options_list = options_opt.GetResult().Cast(); + auto &list_inside = ExtractResultFromParens(options_list.Child(1)); + auto elements = 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] = result->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); + } + } + } + + 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: QualifiedName + // 4: DropBehavior? (CASCADE / RESTRICT; RESTRICT/absent = false) + bool missing_ok = list_pr.Child(2).HasResult(); + auto qname = transformer.Transform(list_pr.Child(3)); + auto server_name = ServerQualifiedNameToDottedString(qname); + bool cascade = false; + auto &behavior = list_pr.Child(4); + if (behavior.HasResult()) { + cascade = transformer.Transform(behavior.GetResult()); + } + + 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_create_user_mapping.cpp b/src/parser/peg/transformer/transform_create_user_mapping.cpp new file mode 100644 index 000000000000..8d1424883324 --- /dev/null +++ b/src/parser/peg/transformer/transform_create_user_mapping.cpp @@ -0,0 +1,144 @@ +#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 { + +// Convert a QualifiedName back into a dotted name. Mirrors the helper used by +// transform_create_server.cpp. +static string UserMappingQualifiedNameToDottedString(const QualifiedName &name) { + string result; + if (!name.Catalog().empty()) { + result += name.Catalog(); + result += "."; + } + if (!name.Schema().empty()) { + result += name.Schema(); + result += "."; + } + result += name.Name(); + return result; +} + +// UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel +// PUBLIC/CURRENT_USER/USER are keyword leaves (emit the lowered keyword text so +// the handler can recognise current_user/user); anything else is a ColLabel and +// is transformed via Transform. The referenced rule can materialise as +// a plain leaf, a single-child List, or a Choice depending on how the framework +// inlines the matched alternative, so unwrap those wrappers before classifying. +static string TransformUserMappingRole(PEGTransformer &transformer, ParseResult &role_result) { + reference node = role_result; + for (;;) { + auto ¤t = node.get(); + if (current.type == ParseResultType::CHOICE) { + node = current.Cast().GetResult(); + continue; + } + if (current.type == ParseResultType::LIST) { + auto &list = current.Cast(); + auto children = list.GetChildren(); + // A UserMappingRole/ColLabel reference with a single child is just a + // wrapper around the matched alternative — descend into it. A multi + // child list is a real rule body that Transform handles. + if (children.size() == 1) { + node = children[0]; + continue; + } + break; + } + break; + } + + auto &matched = node.get(); + if (matched.type == ParseResultType::KEYWORD) { + return StringUtil::Lower(matched.Cast().keyword); + } + if (matched.type == ParseResultType::IDENTIFIER) { + return matched.Cast().identifier; + } + // ColLabel / QualifiedName style nodes resolve through the registered string + // transformer. + return transformer.Transform(matched); +} + +// CREATE USER MAPPING [IF NOT EXISTS] FOR role SERVER name OPTIONS (k 'v', ...) +// -> PRAGMA create_user_mapping('role', 'name', if_not_exists, k := 'v', ...) +unique_ptr PEGTransformerFactory::TransformCreateUserMappingStatement(PEGTransformer &transformer, + ParseResult &parse_result) { + auto &list_pr = parse_result.Cast(); + // Children: + // 0: 'CREATE' + // 1: 'USER' + // 2: 'MAPPING' + // 3: IfNotExists? + // 4: 'FOR' + // 5: UserMappingRole + // 6: 'SERVER' + // 7: QualifiedName + // 8: ServerOptions? + bool if_not_exists = list_pr.Child(3).HasResult(); + auto role = TransformUserMappingRole(transformer, list_pr.GetChild(5)); + auto qname = transformer.Transform(list_pr.Child(7)); + auto server_name = UserMappingQualifiedNameToDottedString(qname); + + auto result = make_uniq(); + result->info->name = "create_user_mapping"; + result->info->parameters.push_back(make_uniq(Value(role))); + result->info->parameters.push_back(make_uniq(Value(server_name))); + result->info->parameters.push_back(make_uniq(Value::BOOLEAN(if_not_exists))); + + // ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) + auto &options_opt = list_pr.Child(8); + if (options_opt.HasResult()) { + auto &options_list = options_opt.GetResult().Cast(); + auto &list_inside = ExtractResultFromParens(options_list.Child(1)); + auto elements = 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. + 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] = result->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); + } + } + } + + return std::move(result); +} + +// DROP USER MAPPING [IF EXISTS] FOR role SERVER name +// -> PRAGMA drop_user_mapping('role', 'name', missing_ok) +unique_ptr PEGTransformerFactory::TransformDropUserMappingStatement(PEGTransformer &transformer, + ParseResult &parse_result) { + auto &list_pr = parse_result.Cast(); + // Children: + // 0: 'DROP' + // 1: 'USER' + // 2: 'MAPPING' + // 3: IfExists? + // 4: 'FOR' + // 5: UserMappingRole + // 6: 'SERVER' + // 7: QualifiedName + bool missing_ok = list_pr.Child(3).HasResult(); + auto role = TransformUserMappingRole(transformer, list_pr.GetChild(5)); + auto qname = transformer.Transform(list_pr.Child(7)); + auto server_name = UserMappingQualifiedNameToDottedString(qname); + + auto result = make_uniq(); + result->info->name = "drop_user_mapping"; + result->info->parameters.push_back(make_uniq(Value(role))); + result->info->parameters.push_back(make_uniq(Value(server_name))); + result->info->parameters.push_back(make_uniq(Value::BOOLEAN(missing_ok))); + return std::move(result); +} + +} // namespace duckdb From ec7ca8b1b155cb9e366a3373c0b00e995db11bc9 Mon Sep 17 00:00:00 2001 From: Pavel && Konstantin Vedernikoffs Date: Wed, 15 Jul 2026 19:33:40 +0000 Subject: [PATCH 2/6] build: drop the serenedb-side clickhouse_scanner config shim serenedb now passes third_party/duckdb_clickhouse/extension_config.cmake through the DUCKDB_EXTENSION_CONFIGS variable (the embedder hook a few lines below the .github/config resolution), so the extension's build config lives with the extension and the fork carries no serenedb wiring. --- .github/config/extensions/clickhouse_scanner.cmake | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .github/config/extensions/clickhouse_scanner.cmake diff --git a/.github/config/extensions/clickhouse_scanner.cmake b/.github/config/extensions/clickhouse_scanner.cmake deleted file mode 100644 index 025623836776..000000000000 --- a/.github/config/extensions/clickhouse_scanner.cmake +++ /dev/null @@ -1,6 +0,0 @@ -if (NOT MINGW AND NOT ${WASM_ENABLED}) - duckdb_extension_load(clickhouse_scanner - SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/duckdb_clickhouse - INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/duckdb_clickhouse/src/include - ) -endif() From 37e27cadc284b8a3361384b7e68badc23ccb3842 Mon Sep 17 00:00:00 2001 From: Pavel && Konstantin Vedernikoffs Date: Wed, 15 Jul 2026 19:51:37 +0000 Subject: [PATCH 3/6] fix(fdw): server names are bare identifiers (ColId), PG-style CREATE/DROP SERVER and the SERVER reference in CREATE/DROP USER MAPPING took a QualifiedName (copied from the schema-qualified tsdict template) and flattened it to a dotted string, so CREATE SERVER a.b.c silently created a server literally named "a.b.c" -- and erased the distinction from the quoted identifier "a.b.c". PG's server_name is a plain ColId and qualification is a syntax error; the grammar now matches, the two copy-pasted *QualifiedNameToDottedString helpers are gone (ColId has a registered Transform, same as CREATE ROLE), and the inlined grammar is regenerated (generated-file format drift reverted). --- .../duckdb/parser/peg/inlined_grammar.gram | 9 +++--- .../duckdb/parser/peg/inlined_grammar.hpp | 8 ++--- .../peg/grammar/statements/create_server.gram | 4 +-- .../statements/create_user_mapping.gram | 4 +-- .../transformer/transform_create_server.cpp | 30 ++++--------------- .../transform_create_user_mapping.cpp | 26 +++------------- 6 files changed, 23 insertions(+), 58 deletions(-) diff --git a/src/include/duckdb/parser/peg/inlined_grammar.gram b/src/include/duckdb/parser/peg/inlined_grammar.gram index 28dd12af214b..fce39586e5f8 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.gram +++ b/src/include/duckdb/parser/peg/inlined_grammar.gram @@ -998,8 +998,9 @@ SeqOwnedBy <- 'OWNED' 'BY' QualifiedName SeqMinOrMax <- MinValue / MaxValue MinValue <- 'MINVALUE' MaxValue <- 'MAXVALUE' -CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? QualifiedName 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? -DropServerStatement <- 'DROP' 'SERVER' IfExists? QualifiedName DropBehavior? + +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? +DropServerStatement <- 'DROP' 'SERVER' IfExists? ColId DropBehavior? ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) ServerOption <- ColLabel StringLiteral @@ -1146,8 +1147,8 @@ CreateTypeFromType <- Type EnumSelectType <- 'ENUM' Parens(SelectStatementInternal) EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?) -CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' QualifiedName ServerOptions? -DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' QualifiedName +CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions? +DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal diff --git a/src/include/duckdb/parser/peg/inlined_grammar.hpp b/src/include/duckdb/parser/peg/inlined_grammar.hpp index 43fed8f61684..6ba655ffa4fe 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.hpp +++ b/src/include/duckdb/parser/peg/inlined_grammar.hpp @@ -936,8 +936,8 @@ const char INLINED_PEG_GRAMMAR[] = { "SeqMinOrMax <- MinValue / MaxValue\n" "MinValue <- 'MINVALUE'\n" "MaxValue <- 'MAXVALUE'\n" - "CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? QualifiedName 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions?\n" - "DropServerStatement <- 'DROP' 'SERVER' IfExists? QualifiedName DropBehavior?\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" @@ -1060,8 +1060,8 @@ const char INLINED_PEG_GRAMMAR[] = { "CreateTypeFromType <- Type\n" "EnumSelectType <- 'ENUM' Parens(SelectStatementInternal)\n" "EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?)\n" - "CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' QualifiedName ServerOptions?\n" - "DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' QualifiedName\n" + "CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions?\n" + "DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId\n" "UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel\n" "CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal\n" "CreateRecursive <- 'RECURSIVE'\n" diff --git a/src/parser/peg/grammar/statements/create_server.gram b/src/parser/peg/grammar/statements/create_server.gram index 360b20729980..fa689d25f0ca 100644 --- a/src/parser/peg/grammar/statements/create_server.gram +++ b/src/parser/peg/grammar/statements/create_server.gram @@ -1,5 +1,5 @@ -CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? QualifiedName 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? -DropServerStatement <- 'DROP' 'SERVER' IfExists? QualifiedName DropBehavior? +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/create_user_mapping.gram b/src/parser/peg/grammar/statements/create_user_mapping.gram index 586b5e7e1ed3..18f6f48aa0d1 100644 --- a/src/parser/peg/grammar/statements/create_user_mapping.gram +++ b/src/parser/peg/grammar/statements/create_user_mapping.gram @@ -1,3 +1,3 @@ -CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' QualifiedName ServerOptions? -DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' QualifiedName +CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions? +DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel diff --git a/src/parser/peg/transformer/transform_create_server.cpp b/src/parser/peg/transformer/transform_create_server.cpp index b5e3552d514a..1d748ccbe4e1 100644 --- a/src/parser/peg/transformer/transform_create_server.cpp +++ b/src/parser/peg/transformer/transform_create_server.cpp @@ -6,40 +6,23 @@ namespace duckdb { -// Convert a QualifiedName back into a dotted name. Mirrors the helper used by -// transform_create_text_search_dictionary.cpp. -static string ServerQualifiedNameToDottedString(const QualifiedName &name) { - string result; - if (!name.Catalog().empty()) { - result += name.Catalog(); - result += "."; - } - if (!name.Schema().empty()) { - result += name.Schema(); - result += "."; - } - result += name.Name(); - return result; -} - // 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) { + ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); // Children: // 0: 'CREATE' // 1: 'SERVER' // 2: IfNotExists? - // 3: QualifiedName + // 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 qname = transformer.Transform(list_pr.Child(3)); - auto server_name = ServerQualifiedNameToDottedString(qname); + 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; @@ -76,16 +59,15 @@ unique_ptr PEGTransformerFactory::TransformCreateServerStatement(P // DROP SERVER [IF EXISTS] name -> PRAGMA drop_foreign_server('name', missing_ok) unique_ptr PEGTransformerFactory::TransformDropServerStatement(PEGTransformer &transformer, - ParseResult &parse_result) { + ParseResult &parse_result) { auto &list_pr = parse_result.Cast(); // Children: // 0: 'DROP', 1: 'SERVER' // 2: IfExists? - // 3: QualifiedName + // 3: ColId (server name) // 4: DropBehavior? (CASCADE / RESTRICT; RESTRICT/absent = false) bool missing_ok = list_pr.Child(2).HasResult(); - auto qname = transformer.Transform(list_pr.Child(3)); - auto server_name = ServerQualifiedNameToDottedString(qname); + auto server_name = transformer.Transform(list_pr.GetChild(3)); bool cascade = false; auto &behavior = list_pr.Child(4); if (behavior.HasResult()) { diff --git a/src/parser/peg/transformer/transform_create_user_mapping.cpp b/src/parser/peg/transformer/transform_create_user_mapping.cpp index 8d1424883324..75d72cc5c35a 100644 --- a/src/parser/peg/transformer/transform_create_user_mapping.cpp +++ b/src/parser/peg/transformer/transform_create_user_mapping.cpp @@ -7,22 +7,6 @@ namespace duckdb { -// Convert a QualifiedName back into a dotted name. Mirrors the helper used by -// transform_create_server.cpp. -static string UserMappingQualifiedNameToDottedString(const QualifiedName &name) { - string result; - if (!name.Catalog().empty()) { - result += name.Catalog(); - result += "."; - } - if (!name.Schema().empty()) { - result += name.Schema(); - result += "."; - } - result += name.Name(); - return result; -} - // UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel // PUBLIC/CURRENT_USER/USER are keyword leaves (emit the lowered keyword text so // the handler can recognise current_user/user); anything else is a ColLabel and @@ -77,12 +61,11 @@ unique_ptr PEGTransformerFactory::TransformCreateUserMappingStatem // 4: 'FOR' // 5: UserMappingRole // 6: 'SERVER' - // 7: QualifiedName + // 7: ColId (server name -- a bare identifier, PG-style) // 8: ServerOptions? bool if_not_exists = list_pr.Child(3).HasResult(); auto role = TransformUserMappingRole(transformer, list_pr.GetChild(5)); - auto qname = transformer.Transform(list_pr.Child(7)); - auto server_name = UserMappingQualifiedNameToDottedString(qname); + auto server_name = transformer.Transform(list_pr.GetChild(7)); auto result = make_uniq(); result->info->name = "create_user_mapping"; @@ -127,11 +110,10 @@ unique_ptr PEGTransformerFactory::TransformDropUserMappingStatemen // 4: 'FOR' // 5: UserMappingRole // 6: 'SERVER' - // 7: QualifiedName + // 7: ColId (server name) bool missing_ok = list_pr.Child(3).HasResult(); auto role = TransformUserMappingRole(transformer, list_pr.GetChild(5)); - auto qname = transformer.Transform(list_pr.Child(7)); - auto server_name = UserMappingQualifiedNameToDottedString(qname); + auto server_name = transformer.Transform(list_pr.GetChild(7)); auto result = make_uniq(); result->info->name = "drop_user_mapping"; From 673b12e6eedc54740be2b89cbdf97ad98ec51b34 Mon Sep 17 00:00:00 2001 From: Pavel && Konstantin Vedernikoffs Date: Thu, 16 Jul 2026 16:11:05 +0000 Subject: [PATCH 4/6] fix(fdw): PG-parity grammar for CREATE SERVER / USER MAPPING - DATA is required: FOREIGN WRAPPER without it is a syntax error (PG) - UserMappingRole accepts CURRENT_ROLE and SESSION_USER (PG auth_ident) - the mapped role is a ColId: reserved keywords are syntax errors, not roles literally named after them --- src/include/duckdb/parser/peg/inlined_grammar.hpp | 4 ++-- src/parser/peg/grammar/statements/create_server.gram | 2 +- .../peg/grammar/statements/create_user_mapping.gram | 2 +- .../peg/transformer/transform_create_user_mapping.cpp | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/include/duckdb/parser/peg/inlined_grammar.hpp b/src/include/duckdb/parser/peg/inlined_grammar.hpp index 6ba655ffa4fe..4dfaf057a39c 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.hpp +++ b/src/include/duckdb/parser/peg/inlined_grammar.hpp @@ -936,7 +936,7 @@ 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" + "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" @@ -1062,7 +1062,7 @@ const char INLINED_PEG_GRAMMAR[] = { "EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?)\n" "CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions?\n" "DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId\n" - "UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel\n" + "UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId\n" "CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal\n" "CreateRecursive <- 'RECURSIVE'\n" "DeallocateStatement <- 'DEALLOCATE' DeallocatePrepare? DeallocateTarget\n" diff --git a/src/parser/peg/grammar/statements/create_server.gram b/src/parser/peg/grammar/statements/create_server.gram index fa689d25f0ca..ba2e9bb37305 100644 --- a/src/parser/peg/grammar/statements/create_server.gram +++ b/src/parser/peg/grammar/statements/create_server.gram @@ -1,4 +1,4 @@ -CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA' 'WRAPPER' Identifier ServerOptions? DropServerStatement <- 'DROP' 'SERVER' IfExists? ColId DropBehavior? ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) diff --git a/src/parser/peg/grammar/statements/create_user_mapping.gram b/src/parser/peg/grammar/statements/create_user_mapping.gram index 18f6f48aa0d1..8a45bb8af6c3 100644 --- a/src/parser/peg/grammar/statements/create_user_mapping.gram +++ b/src/parser/peg/grammar/statements/create_user_mapping.gram @@ -1,3 +1,3 @@ CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions? DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId -UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel +UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId diff --git a/src/parser/peg/transformer/transform_create_user_mapping.cpp b/src/parser/peg/transformer/transform_create_user_mapping.cpp index 75d72cc5c35a..1dff508ff74f 100644 --- a/src/parser/peg/transformer/transform_create_user_mapping.cpp +++ b/src/parser/peg/transformer/transform_create_user_mapping.cpp @@ -7,10 +7,10 @@ namespace duckdb { -// UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel -// PUBLIC/CURRENT_USER/USER are keyword leaves (emit the lowered keyword text so -// the handler can recognise current_user/user); anything else is a ColLabel and -// is transformed via Transform. The referenced rule can materialise as +// UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId +// The special role keywords are leaves (emit the lowered keyword text so the +// handler can recognise them); anything else is a ColId and is transformed via +// Transform. The referenced rule can materialise as // a plain leaf, a single-child List, or a Choice depending on how the framework // inlines the matched alternative, so unwrap those wrappers before classifying. static string TransformUserMappingRole(PEGTransformer &transformer, ParseResult &role_result) { From 299eb7a691891b3d8226856672fa2e742d659572 Mon Sep 17 00:00:00 2001 From: Pavel && Konstantin Vedernikoffs Date: Thu, 16 Jul 2026 16:31:10 +0000 Subject: [PATCH 5/6] refactor(fdw): merge the four FDW transformers into one file transform_create_server.cpp + transform_create_user_mapping.cpp -> transform_foreign_server.cpp: the verbatim-duplicated OPTIONS -> named_parameters loop becomes one shared helper (same emplace order and duplicate-option error); DropBehavior extraction uses the existing TransformOptional; the two unreachable branches in TransformUserMappingRole throw InternalException and its stale parse-shape comment now documents the actual LIST->CHOICE->leaf shape; RegisterCreateServer + RegisterCreateUserMapping -> RegisterForeignServer. --- .../peg/transformer/peg_transformer.hpp | 3 +- src/parser/peg/transformer/CMakeLists.txt | 3 +- .../transformer/peg_transformer_factory.cpp | 12 +- .../transformer/transform_create_server.cpp | 85 --------- .../transform_create_user_mapping.cpp | 126 ------------- .../transformer/transform_foreign_server.cpp | 175 ++++++++++++++++++ 6 files changed, 181 insertions(+), 223 deletions(-) delete mode 100644 src/parser/peg/transformer/transform_create_server.cpp delete mode 100644 src/parser/peg/transformer/transform_create_user_mapping.cpp create mode 100644 src/parser/peg/transformer/transform_foreign_server.cpp diff --git a/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp b/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp index 6ecdd295c0e9..6546a6cdafa8 100644 --- a/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp +++ b/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp @@ -456,8 +456,7 @@ class PEGTransformerFactory { void RegisterNotify(); void RegisterCreateTextSearchDictionary(); void RegisterRbac(); - void RegisterCreateServer(); - void RegisterCreateUserMapping(); + void RegisterForeignServer(); void RegisterPivot(); void RegisterCreateMacro(); void RegisterDrop(); diff --git a/src/parser/peg/transformer/CMakeLists.txt b/src/parser/peg/transformer/CMakeLists.txt index d918f98c6f40..aceee24eb1f6 100644 --- a/src/parser/peg/transformer/CMakeLists.txt +++ b/src/parser/peg/transformer/CMakeLists.txt @@ -24,8 +24,7 @@ add_library_unity( transform_create_publication.cpp transform_create_subscription.cpp transform_notify.cpp - transform_create_server.cpp - transform_create_user_mapping.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 f33bcebd1801..c43dd1f16dbc 100644 --- a/src/parser/peg/transformer/peg_transformer_factory.cpp +++ b/src/parser/peg/transformer/peg_transformer_factory.cpp @@ -199,14 +199,11 @@ void PEGTransformerFactory::RegisterCreateTextSearchDictionary() { REGISTER_TRANSFORM(TransformDropTSDictionaryStatement); } -void PEGTransformerFactory::RegisterCreateServer() { - // create_server.gram — OPTIONS Parens(List(...)) body is not auto-extractable. +void PEGTransformerFactory::RegisterForeignServer() { + // create_server.gram / create_user_mapping.gram — the OPTIONS Parens(List(...)) bodies + // and the user mapping role choice are not auto-extractable. REGISTER_TRANSFORM(TransformCreateServerStatement); REGISTER_TRANSFORM(TransformDropServerStatement); -} - -void PEGTransformerFactory::RegisterCreateUserMapping() { - // create_user_mapping.gram — OPTIONS Parens(List(...)) body and the role choice are not auto-extractable. REGISTER_TRANSFORM(TransformCreateUserMappingStatement); REGISTER_TRANSFORM(TransformDropUserMappingStatement); } @@ -287,8 +284,7 @@ PEGTransformerFactory::PEGTransformerFactory() { RegisterNotify(); RegisterCreateTextSearchDictionary(); RegisterRbac(); - RegisterCreateServer(); - RegisterCreateUserMapping(); + RegisterForeignServer(); RegisterPivot(); RegisterCreateMacro(); RegisterDrop(); diff --git a/src/parser/peg/transformer/transform_create_server.cpp b/src/parser/peg/transformer/transform_create_server.cpp deleted file mode 100644 index 1d748ccbe4e1..000000000000 --- a/src/parser/peg/transformer/transform_create_server.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#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" - -namespace duckdb { - -// 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))); - - // ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) - auto &options_opt = list_pr.Child(8); - if (options_opt.HasResult()) { - auto &options_list = options_opt.GetResult().Cast(); - auto &list_inside = ExtractResultFromParens(options_list.Child(1)); - auto elements = 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] = result->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); - } - } - } - - 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; - auto &behavior = list_pr.Child(4); - if (behavior.HasResult()) { - cascade = transformer.Transform(behavior.GetResult()); - } - - 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_create_user_mapping.cpp b/src/parser/peg/transformer/transform_create_user_mapping.cpp deleted file mode 100644 index 1dff508ff74f..000000000000 --- a/src/parser/peg/transformer/transform_create_user_mapping.cpp +++ /dev/null @@ -1,126 +0,0 @@ -#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 { - -// UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId -// The special role keywords are leaves (emit the lowered keyword text so the -// handler can recognise them); anything else is a ColId and is transformed via -// Transform. The referenced rule can materialise as -// a plain leaf, a single-child List, or a Choice depending on how the framework -// inlines the matched alternative, so unwrap those wrappers before classifying. -static string TransformUserMappingRole(PEGTransformer &transformer, ParseResult &role_result) { - reference node = role_result; - for (;;) { - auto ¤t = node.get(); - if (current.type == ParseResultType::CHOICE) { - node = current.Cast().GetResult(); - continue; - } - if (current.type == ParseResultType::LIST) { - auto &list = current.Cast(); - auto children = list.GetChildren(); - // A UserMappingRole/ColLabel reference with a single child is just a - // wrapper around the matched alternative — descend into it. A multi - // child list is a real rule body that Transform handles. - if (children.size() == 1) { - node = children[0]; - continue; - } - break; - } - break; - } - - auto &matched = node.get(); - if (matched.type == ParseResultType::KEYWORD) { - return StringUtil::Lower(matched.Cast().keyword); - } - if (matched.type == ParseResultType::IDENTIFIER) { - return matched.Cast().identifier; - } - // ColLabel / QualifiedName style nodes resolve through the registered string - // transformer. - return transformer.Transform(matched); -} - -// CREATE USER MAPPING [IF NOT EXISTS] FOR role SERVER name OPTIONS (k 'v', ...) -// -> PRAGMA create_user_mapping('role', 'name', if_not_exists, k := 'v', ...) -unique_ptr PEGTransformerFactory::TransformCreateUserMappingStatement(PEGTransformer &transformer, - ParseResult &parse_result) { - auto &list_pr = parse_result.Cast(); - // Children: - // 0: 'CREATE' - // 1: 'USER' - // 2: 'MAPPING' - // 3: IfNotExists? - // 4: 'FOR' - // 5: UserMappingRole - // 6: 'SERVER' - // 7: ColId (server name -- a bare identifier, PG-style) - // 8: ServerOptions? - bool if_not_exists = list_pr.Child(3).HasResult(); - auto role = TransformUserMappingRole(transformer, list_pr.GetChild(5)); - auto server_name = transformer.Transform(list_pr.GetChild(7)); - - auto result = make_uniq(); - result->info->name = "create_user_mapping"; - result->info->parameters.push_back(make_uniq(Value(role))); - result->info->parameters.push_back(make_uniq(Value(server_name))); - result->info->parameters.push_back(make_uniq(Value::BOOLEAN(if_not_exists))); - - // ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) - auto &options_opt = list_pr.Child(8); - if (options_opt.HasResult()) { - auto &options_list = options_opt.GetResult().Cast(); - auto &list_inside = ExtractResultFromParens(options_list.Child(1)); - auto elements = 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. - 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] = result->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); - } - } - } - - return std::move(result); -} - -// DROP USER MAPPING [IF EXISTS] FOR role SERVER name -// -> PRAGMA drop_user_mapping('role', 'name', missing_ok) -unique_ptr PEGTransformerFactory::TransformDropUserMappingStatement(PEGTransformer &transformer, - ParseResult &parse_result) { - auto &list_pr = parse_result.Cast(); - // Children: - // 0: 'DROP' - // 1: 'USER' - // 2: 'MAPPING' - // 3: IfExists? - // 4: 'FOR' - // 5: UserMappingRole - // 6: 'SERVER' - // 7: ColId (server name) - bool missing_ok = list_pr.Child(3).HasResult(); - auto role = TransformUserMappingRole(transformer, list_pr.GetChild(5)); - auto server_name = transformer.Transform(list_pr.GetChild(7)); - - auto result = make_uniq(); - result->info->name = "drop_user_mapping"; - result->info->parameters.push_back(make_uniq(Value(role))); - result->info->parameters.push_back(make_uniq(Value(server_name))); - result->info->parameters.push_back(make_uniq(Value::BOOLEAN(missing_ok))); - return std::move(result); -} - -} // namespace duckdb 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..2e72ecf276d0 --- /dev/null +++ b/src/parser/peg/transformer/transform_foreign_server.cpp @@ -0,0 +1,175 @@ +#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); + } + } +} + +// UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId +// The special role keywords are leaves; emit the lowered keyword text so the handler +// can recognise them (Transform would return the raw-cased keyword text). +// Anything else is a ColId identifier. The node always materialises as +// LIST -> CHOICE -> leaf, so unwrap those wrappers before classifying. +static string TransformUserMappingRole(ParseResult &role_result) { + reference node = role_result; + for (;;) { + auto ¤t = node.get(); + if (current.type == ParseResultType::CHOICE) { + node = current.Cast().GetResult(); + continue; + } + if (current.type == ParseResultType::LIST) { + auto children = current.Cast().GetChildren(); + if (children.size() != 1) { + throw InternalException("unexpected UserMappingRole parse shape"); + } + node = children[0]; + continue; + } + break; + } + + auto &matched = node.get(); + if (matched.type == ParseResultType::KEYWORD) { + return StringUtil::Lower(matched.Cast().keyword); + } + if (matched.type == ParseResultType::IDENTIFIER) { + return matched.Cast().identifier; + } + throw InternalException("unexpected UserMappingRole parse shape"); +} + +// 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); +} + +// CREATE USER MAPPING [IF NOT EXISTS] FOR role SERVER name OPTIONS (k 'v', ...) +// -> PRAGMA create_user_mapping('role', 'name', if_not_exists, k := 'v', ...) +unique_ptr PEGTransformerFactory::TransformCreateUserMappingStatement(PEGTransformer &transformer, + ParseResult &parse_result) { + auto &list_pr = parse_result.Cast(); + // Children: + // 0: 'CREATE' + // 1: 'USER' + // 2: 'MAPPING' + // 3: IfNotExists? + // 4: 'FOR' + // 5: UserMappingRole + // 6: 'SERVER' + // 7: ColId (server name -- a bare identifier, PG-style) + // 8: ServerOptions? + bool if_not_exists = list_pr.Child(3).HasResult(); + auto role = TransformUserMappingRole(list_pr.GetChild(5)); + auto server_name = transformer.Transform(list_pr.GetChild(7)); + + auto result = make_uniq(); + result->info->name = "create_user_mapping"; + result->info->parameters.push_back(make_uniq(Value(role))); + result->info->parameters.push_back(make_uniq(Value(server_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 USER MAPPING [IF EXISTS] FOR role SERVER name +// -> PRAGMA drop_user_mapping('role', 'name', missing_ok) +unique_ptr PEGTransformerFactory::TransformDropUserMappingStatement(PEGTransformer &transformer, + ParseResult &parse_result) { + auto &list_pr = parse_result.Cast(); + // Children: + // 0: 'DROP' + // 1: 'USER' + // 2: 'MAPPING' + // 3: IfExists? + // 4: 'FOR' + // 5: UserMappingRole + // 6: 'SERVER' + // 7: ColId (server name) + bool missing_ok = list_pr.Child(3).HasResult(); + auto role = TransformUserMappingRole(list_pr.GetChild(5)); + auto server_name = transformer.Transform(list_pr.GetChild(7)); + + auto result = make_uniq(); + result->info->name = "drop_user_mapping"; + result->info->parameters.push_back(make_uniq(Value(role))); + result->info->parameters.push_back(make_uniq(Value(server_name))); + result->info->parameters.push_back(make_uniq(Value::BOOLEAN(missing_ok))); + return std::move(result); +} + +} // namespace duckdb From b71276ad9388e562b6f4eaba9e2b4023b1070a90 Mon Sep 17 00:00:00 2001 From: Pavel && Konstantin Vedernikoffs Date: Fri, 17 Jul 2026 23:11:12 +0000 Subject: [PATCH 6/6] feat(fdw): delete USER MAPPING grammar; add GRANT ... ON FOREIGN SERVER Server credentials now live entirely in CREATE SERVER OPTIONS (ClickHouse named-collection style), so USER MAPPING is gone: create_user_mapping.gram deleted, the CREATE/DROP USER MAPPING statements + their transformers removed. GrantObjType gains FOREIGN SERVER so 'GRANT USAGE ON FOREIGN SERVER s TO role' parses (the multi-keyword object type is joined in the rbac transformer). --- .../duckdb/parser/peg/inlined_grammar.gram | 8 +- .../duckdb/parser/peg/inlined_grammar.hpp | 8 +- .../peg/transformer/peg_transformer.hpp | 6 -- src/parser/peg/grammar/statements/common.gram | 2 - .../statements/create_user_mapping.gram | 3 - src/parser/peg/grammar/statements/rbac.gram | 3 +- .../transformer/peg_transformer_factory.cpp | 5 +- .../transformer/transform_foreign_server.cpp | 88 ------------------- src/parser/peg/transformer/transform_rbac.cpp | 19 ++-- 9 files changed, 19 insertions(+), 123 deletions(-) delete mode 100644 src/parser/peg/grammar/statements/create_user_mapping.gram diff --git a/src/include/duckdb/parser/peg/inlined_grammar.gram b/src/include/duckdb/parser/peg/inlined_grammar.gram index fce39586e5f8..f48e4c6a5e45 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.gram +++ b/src/include/duckdb/parser/peg/inlined_grammar.gram @@ -9,8 +9,6 @@ Statement <- DropTSDictionaryStatement / CreateServerStatement / DropServerStatement / - CreateUserMappingStatement / - DropUserMappingStatement / CreateRoleStatement / DropRoleStatement / CreatePolicyStatement / @@ -999,7 +997,7 @@ SeqMinOrMax <- MinValue / MaxValue MinValue <- 'MINVALUE' MaxValue <- 'MAXVALUE' -CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA'? 'WRAPPER' Identifier ServerOptions? +CreateServerStatement <- 'CREATE' 'SERVER' IfNotExists? ColId 'FOREIGN' 'DATA' 'WRAPPER' Identifier ServerOptions? DropServerStatement <- 'DROP' 'SERVER' IfExists? ColId DropBehavior? ServerOptions <- 'OPTIONS' Parens(List(ServerOption)) @@ -1147,10 +1145,6 @@ CreateTypeFromType <- Type EnumSelectType <- 'ENUM' Parens(SelectStatementInternal) EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?) -CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions? -DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId -UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'USER' / ColLabel - CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal CreateRecursive <- 'RECURSIVE' diff --git a/src/include/duckdb/parser/peg/inlined_grammar.hpp b/src/include/duckdb/parser/peg/inlined_grammar.hpp index 4dfaf057a39c..08a5b1b0428b 100644 --- a/src/include/duckdb/parser/peg/inlined_grammar.hpp +++ b/src/include/duckdb/parser/peg/inlined_grammar.hpp @@ -13,8 +13,6 @@ const char INLINED_PEG_GRAMMAR[] = { " DropTSDictionaryStatement /\n" " CreateServerStatement /\n" " DropServerStatement /\n" - " CreateUserMappingStatement /\n" - " DropUserMappingStatement /\n" " CreateRoleStatement /\n" " DropRoleStatement /\n" " CreatePolicyStatement /\n" @@ -1060,9 +1058,6 @@ const char INLINED_PEG_GRAMMAR[] = { "CreateTypeFromType <- Type\n" "EnumSelectType <- 'ENUM' Parens(SelectStatementInternal)\n" "EnumStringLiteralList <- 'ENUM' Parens(List(StringLiteral)?)\n" - "CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions?\n" - "DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId\n" - "UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId\n" "CreateViewStmt <- CreateRecursive? 'VIEW' IfNotExists? QualifiedName InsertColumnList? WithList? 'AS' SelectStatementInternal\n" "CreateRecursive <- 'RECURSIVE'\n" "DeallocateStatement <- 'DEALLOCATE' DeallocatePrepare? DeallocateTarget\n" @@ -1684,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 6546a6cdafa8..caab1bfa347e 100644 --- a/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp +++ b/src/include/duckdb/parser/peg/transformer/peg_transformer.hpp @@ -530,12 +530,6 @@ class PEGTransformerFactory { ParseResult &parse_result); static unique_ptr TransformDropServerStatement(PEGTransformer &transformer, ParseResult &parse_result); - // create_user_mapping.gram — CREATE/DROP USER MAPPING reuse the OPTIONS Parens(List(...)) body and a - // PUBLIC/CURRENT_USER/USER/role choice that the generator cannot auto-extract; hand-write the entry points. - static unique_ptr TransformCreateUserMappingStatement(PEGTransformer &transformer, - ParseResult &parse_result); - static unique_ptr TransformDropUserMappingStatement(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 7d0c622749a6..2c66e52840a0 100644 --- a/src/parser/peg/grammar/statements/common.gram +++ b/src/parser/peg/grammar/statements/common.gram @@ -9,8 +9,6 @@ Statement <- DropTSDictionaryStatement / CreateServerStatement / DropServerStatement / - CreateUserMappingStatement / - DropUserMappingStatement / CreateRoleStatement / DropRoleStatement / CreatePolicyStatement / diff --git a/src/parser/peg/grammar/statements/create_user_mapping.gram b/src/parser/peg/grammar/statements/create_user_mapping.gram deleted file mode 100644 index 8a45bb8af6c3..000000000000 --- a/src/parser/peg/grammar/statements/create_user_mapping.gram +++ /dev/null @@ -1,3 +0,0 @@ -CreateUserMappingStatement <- 'CREATE' 'USER' 'MAPPING' IfNotExists? 'FOR' UserMappingRole 'SERVER' ColId ServerOptions? -DropUserMappingStatement <- 'DROP' 'USER' 'MAPPING' IfExists? 'FOR' UserMappingRole 'SERVER' ColId -UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId 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/peg_transformer_factory.cpp b/src/parser/peg/transformer/peg_transformer_factory.cpp index c43dd1f16dbc..bab644aee6f0 100644 --- a/src/parser/peg/transformer/peg_transformer_factory.cpp +++ b/src/parser/peg/transformer/peg_transformer_factory.cpp @@ -200,12 +200,9 @@ void PEGTransformerFactory::RegisterCreateTextSearchDictionary() { } void PEGTransformerFactory::RegisterForeignServer() { - // create_server.gram / create_user_mapping.gram — the OPTIONS Parens(List(...)) bodies - // and the user mapping role choice are not auto-extractable. + // create_server.gram — the OPTIONS Parens(List(...)) body is not auto-extractable. REGISTER_TRANSFORM(TransformCreateServerStatement); REGISTER_TRANSFORM(TransformDropServerStatement); - REGISTER_TRANSFORM(TransformCreateUserMappingStatement); - REGISTER_TRANSFORM(TransformDropUserMappingStatement); } void PEGTransformerFactory::RegisterPivot() { diff --git a/src/parser/peg/transformer/transform_foreign_server.cpp b/src/parser/peg/transformer/transform_foreign_server.cpp index 2e72ecf276d0..bad69bbe877a 100644 --- a/src/parser/peg/transformer/transform_foreign_server.cpp +++ b/src/parser/peg/transformer/transform_foreign_server.cpp @@ -33,40 +33,6 @@ static void TransformServerOptionsInto(PEGTransformer &transformer, ListParseRes } } -// UserMappingRole <- 'PUBLIC' / 'CURRENT_USER' / 'CURRENT_ROLE' / 'SESSION_USER' / 'USER' / ColId -// The special role keywords are leaves; emit the lowered keyword text so the handler -// can recognise them (Transform would return the raw-cased keyword text). -// Anything else is a ColId identifier. The node always materialises as -// LIST -> CHOICE -> leaf, so unwrap those wrappers before classifying. -static string TransformUserMappingRole(ParseResult &role_result) { - reference node = role_result; - for (;;) { - auto ¤t = node.get(); - if (current.type == ParseResultType::CHOICE) { - node = current.Cast().GetResult(); - continue; - } - if (current.type == ParseResultType::LIST) { - auto children = current.Cast().GetChildren(); - if (children.size() != 1) { - throw InternalException("unexpected UserMappingRole parse shape"); - } - node = children[0]; - continue; - } - break; - } - - auto &matched = node.get(); - if (matched.type == ParseResultType::KEYWORD) { - return StringUtil::Lower(matched.Cast().keyword); - } - if (matched.type == ParseResultType::IDENTIFIER) { - return matched.Cast().identifier; - } - throw InternalException("unexpected UserMappingRole parse shape"); -} - // 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, @@ -118,58 +84,4 @@ unique_ptr PEGTransformerFactory::TransformDropServerStatement(PEG return std::move(result); } -// CREATE USER MAPPING [IF NOT EXISTS] FOR role SERVER name OPTIONS (k 'v', ...) -// -> PRAGMA create_user_mapping('role', 'name', if_not_exists, k := 'v', ...) -unique_ptr PEGTransformerFactory::TransformCreateUserMappingStatement(PEGTransformer &transformer, - ParseResult &parse_result) { - auto &list_pr = parse_result.Cast(); - // Children: - // 0: 'CREATE' - // 1: 'USER' - // 2: 'MAPPING' - // 3: IfNotExists? - // 4: 'FOR' - // 5: UserMappingRole - // 6: 'SERVER' - // 7: ColId (server name -- a bare identifier, PG-style) - // 8: ServerOptions? - bool if_not_exists = list_pr.Child(3).HasResult(); - auto role = TransformUserMappingRole(list_pr.GetChild(5)); - auto server_name = transformer.Transform(list_pr.GetChild(7)); - - auto result = make_uniq(); - result->info->name = "create_user_mapping"; - result->info->parameters.push_back(make_uniq(Value(role))); - result->info->parameters.push_back(make_uniq(Value(server_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 USER MAPPING [IF EXISTS] FOR role SERVER name -// -> PRAGMA drop_user_mapping('role', 'name', missing_ok) -unique_ptr PEGTransformerFactory::TransformDropUserMappingStatement(PEGTransformer &transformer, - ParseResult &parse_result) { - auto &list_pr = parse_result.Cast(); - // Children: - // 0: 'DROP' - // 1: 'USER' - // 2: 'MAPPING' - // 3: IfExists? - // 4: 'FOR' - // 5: UserMappingRole - // 6: 'SERVER' - // 7: ColId (server name) - bool missing_ok = list_pr.Child(3).HasResult(); - auto role = TransformUserMappingRole(list_pr.GetChild(5)); - auto server_name = transformer.Transform(list_pr.GetChild(7)); - - auto result = make_uniq(); - result->info->name = "drop_user_mapping"; - result->info->parameters.push_back(make_uniq(Value(role))); - result->info->parameters.push_back(make_uniq(Value(server_name))); - result->info->parameters.push_back(make_uniq(Value::BOOLEAN(missing_ok))); - 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))}; }