diff --git a/ydb/core/kqp/gateway/behaviour/view/manager.cpp b/ydb/core/kqp/gateway/behaviour/view/manager.cpp index 069697d53baf..f7d421a790ae 100644 --- a/ydb/core/kqp/gateway/behaviour/view/manager.cpp +++ b/ydb/core/kqp/gateway/behaviour/view/manager.cpp @@ -60,6 +60,7 @@ void FillCreateViewProposal(NKikimrSchemeOp::TModifyScheme& modifyScheme, const auto pathPair = SplitPathByDb(settings.GetObjectId(), context.GetDatabase()); modifyScheme.SetWorkingDir(pathPair.first); modifyScheme.SetOperationType(NKikimrSchemeOp::ESchemeOpCreateView); + modifyScheme.SetFailedOnAlreadyExists(!settings.GetExistingOk()); auto& viewDesc = *modifyScheme.MutableCreateView(); viewDesc.SetName(pathPair.second); @@ -77,6 +78,7 @@ void FillDropViewProposal(NKikimrSchemeOp::TModifyScheme& modifyScheme, const auto pathPair = SplitPathByObjectId(settings.GetObjectId()); modifyScheme.SetWorkingDir(pathPair.first); modifyScheme.SetOperationType(NKikimrSchemeOp::ESchemeOpDropView); + modifyScheme.SetSuccessOnNotExist(settings.GetMissingOk()); auto& drop = *modifyScheme.MutableDrop(); drop.SetName(pathPair.second); @@ -84,9 +86,12 @@ void FillDropViewProposal(NKikimrSchemeOp::TModifyScheme& modifyScheme, NThreading::TFuture SendSchemeRequest(TEvTxUserProxy::TEvProposeTransaction* request, TActorSystem* actorSystem, - bool failOnAlreadyExists) { + bool failedOnAlreadyExists, + bool successOnNotExist) { const auto promiseScheme = NThreading::NewPromise(); - IActor* const requestHandler = new TSchemeOpRequestHandler(request, promiseScheme, failOnAlreadyExists); + IActor* const requestHandler = new TSchemeOpRequestHandler( + request, promiseScheme, failedOnAlreadyExists, successOnNotExist + ); actorSystem->Register(requestHandler); return promiseScheme.GetFuture().Apply([](const NThreading::TFuture& opResult) { if (opResult.HasValue()) { @@ -109,7 +114,12 @@ NThreading::TFuture CreateView(const NYql::TCreateObjectSe auto& schemeTx = *proposal->Record.MutableTransaction()->MutableModifyScheme(); FillCreateViewProposal(schemeTx, settings, context.GetExternalData()); - return SendSchemeRequest(proposal.Release(), context.GetExternalData().GetActorSystem(), true); + return SendSchemeRequest( + proposal.Release(), + context.GetExternalData().GetActorSystem(), + schemeTx.GetFailedOnAlreadyExists(), + schemeTx.GetSuccessOnNotExist() + ); } NThreading::TFuture DropView(const NYql::TDropObjectSettings& settings, @@ -122,7 +132,12 @@ NThreading::TFuture DropView(const NYql::TDropObjectSettin auto& schemeTx = *proposal->Record.MutableTransaction()->MutableModifyScheme(); FillDropViewProposal(schemeTx, settings); - return SendSchemeRequest(proposal.Release(), context.GetExternalData().GetActorSystem(), false); + return SendSchemeRequest( + proposal.Release(), + context.GetExternalData().GetActorSystem(), + schemeTx.GetFailedOnAlreadyExists(), + schemeTx.GetSuccessOnNotExist() + ); } void PrepareCreateView(NKqpProto::TKqpSchemeOperation& schemeOperation, @@ -214,10 +229,10 @@ NThreading::TFuture TViewManager::ExecutePrepared(const NK switch (schemeOperation.GetOperationCase()) { case NKqpProto::TKqpSchemeOperation::kCreateView: schemeTx.CopyFrom(schemeOperation.GetCreateView()); - return SendSchemeRequest(proposal.Release(), context.GetActorSystem(), true); + break; case NKqpProto::TKqpSchemeOperation::kDropView: schemeTx.CopyFrom(schemeOperation.GetDropView()); - return SendSchemeRequest(proposal.Release(), context.GetActorSystem(), false); + break; default: return NThreading::MakeFuture(TYqlConclusionStatus::Fail( TStringBuilder() @@ -226,6 +241,12 @@ NThreading::TFuture TViewManager::ExecutePrepared(const NK ) ); } + return SendSchemeRequest( + proposal.Release(), + context.GetActorSystem(), + schemeTx.GetFailedOnAlreadyExists(), + schemeTx.GetSuccessOnNotExist() + ); } } diff --git a/ydb/core/kqp/ut/view/view_ut.cpp b/ydb/core/kqp/ut/view/view_ut.cpp index ba7aca97e589..cb55869e4bf2 100644 --- a/ydb/core/kqp/ut/view/view_ut.cpp +++ b/ydb/core/kqp/ut/view/view_ut.cpp @@ -344,10 +344,60 @@ Y_UNIT_TEST_SUITE(TCreateAndDropViewTest) { { const auto creationResult = session.ExecuteSchemeQuery(creationQuery).GetValueSync(); UNIT_ASSERT(!creationResult.IsSuccess()); - UNIT_ASSERT(creationResult.GetIssues().ToString().Contains("error: path exist, request accepts it")); + UNIT_ASSERT_STRING_CONTAINS(creationResult.GetIssues().ToString(), "error: path exist, request accepts it"); } } + Y_UNIT_TEST(CreateViewOccupiedName) { + TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); + EnableViewsFeatureFlag(kikimr); + auto session = kikimr.GetQueryClient().GetSession().ExtractValueSync().GetSession(); + + constexpr const char* path = "table"; + + const TString createTable = std::format(R"( + CREATE TABLE {} (key Int32, value Utf8, PRIMARY KEY (key)); + )", path + ); + ExecuteQuery(session, createTable); + + auto checkError = [&session](const TString& query, const TString& expectedError) { + const auto result = session.ExecuteQuery(query, NQuery::TTxControl::NoTx()).ExtractValueSync(); + UNIT_ASSERT(!result.IsSuccess()); + UNIT_ASSERT_STRING_CONTAINS(result.GetIssues().ToString(), expectedError); + }; + + const TString queryTemplate = std::format(R"( + CREATE VIEW {{}}{} WITH (security_invoker = true) AS SELECT 1; + )", path + ); + const TString expectedError = std::format("path: '/Root/{}', error: unexpected path type", path); + + for (std::string existenceCheck : {"", "IF NOT EXISTS "}) { + const TString createView = std::vformat(queryTemplate, std::make_format_args(existenceCheck)); + checkError(createView, expectedError); + } + } + + Y_UNIT_TEST(CreateViewIfNotExists) { + TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); + EnableViewsFeatureFlag(kikimr); + auto session = kikimr.GetQueryClient().GetSession().ExtractValueSync().GetSession(); + + constexpr const char* path = "/Root/TheView"; + constexpr const char* queryInView = "SELECT 1"; + + const TString creationQuery = std::format(R"( + CREATE VIEW IF NOT EXISTS `{}` WITH (security_invoker = true) AS {}; + )", + path, + queryInView + ); + ExecuteQuery(session, creationQuery); + // an attempt to create a duplicate does not produce an error + ExecuteQuery(session, creationQuery); + } + Y_UNIT_TEST(DropView) { TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); EnableViewsFeatureFlag(kikimr); @@ -399,6 +449,42 @@ Y_UNIT_TEST_SUITE(TCreateAndDropViewTest) { UNIT_ASSERT_STRING_CONTAINS(dropResult.GetIssues().ToString(), "Error: Views are disabled"); } + Y_UNIT_TEST(DropNonexistingView) { + TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); + EnableViewsFeatureFlag(kikimr); + auto session = kikimr.GetQueryClient().GetSession().ExtractValueSync().GetSession(); + + const auto dropResult = session.ExecuteQuery( + "DROP VIEW NonexistingView;", NQuery::TTxControl::NoTx() + ).ExtractValueSync(); + + UNIT_ASSERT(!dropResult.IsSuccess()); + UNIT_ASSERT_STRING_CONTAINS(dropResult.GetIssues().ToString(), "Error: Path does not exist"); + } + + Y_UNIT_TEST(CallDropViewOnTable) { + TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); + EnableViewsFeatureFlag(kikimr); + auto session = kikimr.GetQueryClient().GetSession().ExtractValueSync().GetSession(); + + constexpr const char* path = "table"; + + const TString createTable = std::format(R"( + CREATE TABLE {} (key Int32, value Utf8, PRIMARY KEY (key)); + )", path + ); + ExecuteQuery(session, createTable); + + auto checkError = [&session](const TString& query, const TString& expectedError) { + const auto result = session.ExecuteQuery(query, NQuery::TTxControl::NoTx()).ExtractValueSync(); + UNIT_ASSERT(!result.IsSuccess()); + UNIT_ASSERT_STRING_CONTAINS(result.GetIssues().ToString(), expectedError); + }; + const TString expectedError = std::format("path: '/Root/{}', error: path is not a view", path); + checkError(std::format("DROP VIEW {};", path), expectedError); + checkError(std::format("DROP VIEW IF EXISTS {};", path), expectedError); + } + Y_UNIT_TEST(DropSameViewTwice) { TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); EnableViewsFeatureFlag(kikimr); @@ -424,10 +510,36 @@ Y_UNIT_TEST_SUITE(TCreateAndDropViewTest) { { const auto dropResult = session.ExecuteSchemeQuery(dropQuery).GetValueSync(); UNIT_ASSERT(!dropResult.IsSuccess()); - UNIT_ASSERT(dropResult.GetIssues().ToString().Contains("Error: Path does not exist")); + UNIT_ASSERT_STRING_CONTAINS(dropResult.GetIssues().ToString(), "Error: Path does not exist"); } } + Y_UNIT_TEST(DropViewIfExists) { + TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); + EnableViewsFeatureFlag(kikimr); + auto session = kikimr.GetQueryClient().GetSession().ExtractValueSync().GetSession(); + + constexpr const char* path = "/Root/TheView"; + constexpr const char* queryInView = "SELECT 1"; + + const TString creationQuery = std::format(R"( + CREATE VIEW `{}` WITH (security_invoker = true) AS {}; + )", + path, + queryInView + ); + ExecuteQuery(session, creationQuery); + + const TString dropQuery = std::format(R"( + DROP VIEW IF EXISTS `{}`; + )", + path + ); + ExecuteQuery(session, dropQuery); + // an attempt to drop an already deleted view does not produce an error + ExecuteQuery(session, dropQuery); + } + Y_UNIT_TEST(DropViewInFolder) { TKikimrRunner kikimr(TKikimrSettings().SetWithSampleTables(false)); EnableViewsFeatureFlag(kikimr); diff --git a/ydb/library/yql/sql/v1/SQLv1.g.in b/ydb/library/yql/sql/v1/SQLv1.g.in index 76b691b81e56..f92b02c8babd 100644 --- a/ydb/library/yql/sql/v1/SQLv1.g.in +++ b/ydb/library/yql/sql/v1/SQLv1.g.in @@ -596,12 +596,12 @@ alter_external_data_source_action: drop_external_data_source_stmt: DROP EXTERNAL DATA SOURCE (IF EXISTS)? object_ref; -create_view_stmt: CREATE VIEW object_ref +create_view_stmt: CREATE VIEW (IF NOT EXISTS)? object_ref create_object_features? AS select_stmt ; -drop_view_stmt: DROP VIEW object_ref; +drop_view_stmt: DROP VIEW (IF EXISTS)? object_ref; upsert_object_stmt: UPSERT OBJECT object_ref LPAREN TYPE object_type_ref RPAREN diff --git a/ydb/library/yql/sql/v1/format/sql_format_ut.cpp b/ydb/library/yql/sql/v1/format/sql_format_ut.cpp index 6f589f325a12..8d7c91805880 100644 --- a/ydb/library/yql/sql/v1/format/sql_format_ut.cpp +++ b/ydb/library/yql/sql/v1/format/sql_format_ut.cpp @@ -1579,9 +1579,16 @@ FROM Input MATCH_RECOGNIZE (PATTERN (A) DEFINE A AS A); } Y_UNIT_TEST(CreateView) { - TCases cases = { - {"creAte vIEw TheView wiTh (security_invoker = trUE) As SELect 1", - "CREATE VIEW TheView WITH (security_invoker = TRUE) AS\nSELECT\n\t1;\n"}, + TCases cases = {{ + "creAte vIEw TheView As SELect 1", + "CREATE VIEW TheView AS\nSELECT\n\t1;\n" + }, { + "creAte vIEw If Not ExIsTs TheView As SELect 1", + "CREATE VIEW IF NOT EXISTS TheView AS\nSELECT\n\t1;\n" + }, { + "creAte vIEw TheView wiTh (option = tRuE) As SELect 1", + "CREATE VIEW TheView WITH (option = TRUE) AS\nSELECT\n\t1;\n" + } }; TSetup setup; @@ -1589,9 +1596,13 @@ FROM Input MATCH_RECOGNIZE (PATTERN (A) DEFINE A AS A); } Y_UNIT_TEST(DropView) { - TCases cases = { - {"dRop viEW theVIEW", - "DROP VIEW theVIEW;\n"}, + TCases cases = {{ + "dRop viEW theVIEW", + "DROP VIEW theVIEW;\n" + }, { + "dRop viEW iF EXistS theVIEW", + "DROP VIEW IF EXISTS theVIEW;\n" + } }; TSetup setup; diff --git a/ydb/library/yql/sql/v1/sql_query.cpp b/ydb/library/yql/sql/v1/sql_query.cpp index f2c0a9e26e04..40c1b6727204 100644 --- a/ydb/library/yql/sql/v1/sql_query.cpp +++ b/ydb/library/yql/sql/v1/sql_query.cpp @@ -1220,11 +1220,11 @@ bool TSqlQuery::Statement(TVector& blocks, const TRule_sql_stmt_core& break; } case TRule_sql_stmt_core::kAltSqlStmtCore42: { - // create_view_stmt: CREATE VIEW name WITH (k = v, ...) AS select_stmt; + // create_view_stmt: CREATE VIEW (IF NOT EXISTS)? name (WITH (k = v, ...))? AS select_stmt; auto& node = core.GetAlt_sql_stmt_core42().GetRule_create_view_stmt1(); TObjectOperatorContext context(Ctx.Scoped); - if (node.GetRule_object_ref3().HasBlock1()) { - if (!ClusterExpr(node.GetRule_object_ref3().GetBlock1().GetRule_cluster_expr1(), + if (node.GetRule_object_ref4().HasBlock1()) { + if (!ClusterExpr(node.GetRule_object_ref4().GetBlock1().GetRule_cluster_expr1(), false, context.ServiceId, context.Cluster)) { @@ -1232,34 +1232,36 @@ bool TSqlQuery::Statement(TVector& blocks, const TRule_sql_stmt_core& } } + const bool existingOk = node.HasBlock3(); + std::map features; - if (node.HasBlock4()) { - if (!ParseObjectFeatures(features, node.GetBlock4().GetRule_create_object_features1().GetRule_object_features2())) { + if (node.HasBlock5()) { + if (!ParseObjectFeatures(features, node.GetBlock5().GetRule_create_object_features1().GetRule_object_features2())) { return false; } } - if (!ParseViewQuery(features, node.GetRule_select_stmt6())) { + if (!ParseViewQuery(features, node.GetRule_select_stmt7())) { return false; } - const TString objectId = Id(node.GetRule_object_ref3().GetRule_id_or_at2(), *this).second; + const TString objectId = Id(node.GetRule_object_ref4().GetRule_id_or_at2(), *this).second; constexpr const char* TypeId = "VIEW"; AddStatementToBlocks(blocks, BuildCreateObjectOperation(Ctx.Pos(), BuildTablePath(Ctx.GetPrefixPath(context.ServiceId, context.Cluster), objectId), TypeId, - false, + existingOk, false, std::move(features), context)); break; } case TRule_sql_stmt_core::kAltSqlStmtCore43: { - // drop_view_stmt: DROP VIEW name; + // drop_view_stmt: DROP VIEW (IF EXISTS)? name; auto& node = core.GetAlt_sql_stmt_core43().GetRule_drop_view_stmt1(); TObjectOperatorContext context(Ctx.Scoped); - if (node.GetRule_object_ref3().HasBlock1()) { - if (!ClusterExpr(node.GetRule_object_ref3().GetBlock1().GetRule_cluster_expr1(), + if (node.GetRule_object_ref4().HasBlock1()) { + if (!ClusterExpr(node.GetRule_object_ref4().GetBlock1().GetRule_cluster_expr1(), false, context.ServiceId, context.Cluster)) { @@ -1267,13 +1269,15 @@ bool TSqlQuery::Statement(TVector& blocks, const TRule_sql_stmt_core& } } - const TString objectId = Id(node.GetRule_object_ref3().GetRule_id_or_at2(), *this).second; + const bool missingOk = node.HasBlock3(); + + const TString objectId = Id(node.GetRule_object_ref4().GetRule_id_or_at2(), *this).second; constexpr const char* TypeId = "VIEW"; AddStatementToBlocks(blocks, BuildDropObjectOperation(Ctx.Pos(), BuildTablePath(Ctx.GetPrefixPath(context.ServiceId, context.Cluster), objectId), TypeId, - false, + missingOk, {}, context)); break; diff --git a/ydb/library/yql/sql/v1/sql_ut.cpp b/ydb/library/yql/sql/v1/sql_ut.cpp index 5fda9c17726d..1570fabfc4b7 100644 --- a/ydb/library/yql/sql/v1/sql_ut.cpp +++ b/ydb/library/yql/sql/v1/sql_ut.cpp @@ -2691,7 +2691,7 @@ Y_UNIT_TEST_SUITE(SqlParsingOnly) { auto req = Sprintf(reqTpl, key.c_str(), value.c_str()); auto res = SqlToYql(req); UNIT_ASSERT(res.Root); - + TVerifyLineFunc verifyLine = [&key, &value](const TString& word, const TString& line) { if (word == "Write") { UNIT_ASSERT_VALUES_UNEQUAL(TString::npos, line.find("MyReplication")); @@ -6592,6 +6592,28 @@ Y_UNIT_TEST_SUITE(TViewSyntaxTest) { UNIT_ASSERT_C(res.Root, res.Issues.ToString()); } + Y_UNIT_TEST(CreateViewIfNotExists) { + constexpr const char* name = "TheView"; + NYql::TAstParseResult res = SqlToYql(std::format(R"( + USE plato; + CREATE VIEW IF NOT EXISTS {} WITH (security_invoker = TRUE) AS SELECT 1; + )", name + )); + UNIT_ASSERT_C(res.Root, res.Issues.ToString()); + + TVerifyLineFunc verifyLine = [&](const TString& word, const TString& line) { + if (word == "Write!") { + UNIT_ASSERT_STRING_CONTAINS(line, name); + UNIT_ASSERT_STRING_CONTAINS(line, "createObjectIfNotExists"); + } + }; + + TWordCountHive elementStat = { {"Write!"} }; + VerifyProgram(res, elementStat, verifyLine); + + UNIT_ASSERT_VALUES_EQUAL(elementStat["Write!"], 1); + } + Y_UNIT_TEST(CreateViewFromTable) { constexpr const char* path = "/PathPrefix/TheView"; constexpr const char* query = R"( @@ -6671,6 +6693,28 @@ Y_UNIT_TEST_SUITE(TViewSyntaxTest) { UNIT_ASSERT_VALUES_EQUAL(elementStat["Write!"], 1); } + Y_UNIT_TEST(DropViewIfExists) { + constexpr const char* name = "TheView"; + NYql::TAstParseResult res = SqlToYql(std::format(R"( + USE plato; + DROP VIEW IF EXISTS {}; + )", name + )); + UNIT_ASSERT_C(res.Root, res.Issues.ToString()); + + TVerifyLineFunc verifyLine = [&](const TString& word, const TString& line) { + if (word == "Write!") { + UNIT_ASSERT_STRING_CONTAINS(line, name); + UNIT_ASSERT_STRING_CONTAINS(line, "dropObjectIfExists"); + } + }; + + TWordCountHive elementStat = { {"Write!"} }; + VerifyProgram(res, elementStat, verifyLine); + + UNIT_ASSERT_VALUES_EQUAL(elementStat["Write!"], 1); + } + Y_UNIT_TEST(CreateViewWithTablePrefix) { NYql::TAstParseResult res = SqlToYql(R"( USE plato; @@ -6714,7 +6758,7 @@ Y_UNIT_TEST_SUITE(TViewSyntaxTest) { UNIT_ASSERT_VALUES_EQUAL(elementStat["Write!"], 1); } - + Y_UNIT_TEST(YtAlternativeSchemaSyntax) { NYql::TAstParseResult res = SqlToYql(R"( SELECT * FROM plato.Input WITH schema(y Int32, x String not null); @@ -6783,7 +6827,7 @@ Y_UNIT_TEST_SUITE(CompactNamedExprs) { pragma CompactNamedExprs; pragma ValidateUnusedExprs; - define subquery $x() as + define subquery $x() as select count(1, 2); end define; select 1; @@ -6806,7 +6850,7 @@ Y_UNIT_TEST_SUITE(CompactNamedExprs) { pragma CompactNamedExprs; pragma DisableValidateUnusedExprs; - define subquery $x() as + define subquery $x() as select count(1, 2); end define; select 1;