@@ -377,6 +377,25 @@ TString ExecuteReturningQuery(TKikimrRunner& kikimr, bool queryService, TString
377377 return FormatResultSetYson (result.GetResultSet (0 ));
378378}
379379
380+ TString ExecuteReturningQueryWithParams (TKikimrRunner& kikimr, bool queryService, TString query, const TParams& params) {
381+ if (queryService) {
382+ auto qdb = kikimr.GetQueryClient ();
383+ auto qSession = qdb.GetSession ().GetValueSync ().GetSession ();
384+ auto settings = NYdb::NQuery::TExecuteQuerySettings ()
385+ .Syntax (NYdb::NQuery::ESyntax::YqlV1);
386+ auto result = qSession.ExecuteQuery (
387+ query, NYdb::NQuery::TTxControl::BeginTx ().CommitTx (), params, settings).ExtractValueSync ();
388+ UNIT_ASSERT_VALUES_EQUAL_C (result.GetStatus (), EStatus::SUCCESS, result.GetIssues ().ToString ());
389+ return FormatResultSetYson (result.GetResultSet (0 ));
390+ }
391+
392+ auto db = kikimr.GetTableClient ();
393+ auto session = db.CreateSession ().GetValueSync ().GetSession ();
394+ auto result = session.ExecuteDataQuery (query, TTxControl::BeginTx ().CommitTx (), params).ExtractValueSync ();
395+ UNIT_ASSERT_VALUES_EQUAL_C (result.GetStatus (), EStatus::SUCCESS, result.GetIssues ().ToString ());
396+ return FormatResultSetYson (result.GetResultSet (0 ));
397+ }
398+
380399Y_UNIT_TEST_TWIN (ReturningWorks, QueryService) {
381400 auto kikimr = DefaultKikimrRunner ();
382401 auto db = kikimr.GetTableClient ();
@@ -663,6 +682,291 @@ Y_UNIT_TEST(ReturningTypes) {
663682 }
664683}
665684
685+ Y_UNIT_TEST_TWIN (ReturningUpsertAsTableListNotNullOnly, QueryService) {
686+ // Test for issue #27021: Query fails when using RETURNING CLAUSE with UPSERT
687+ // to table with only NOT NULL fields and query parameters of type List
688+ auto kikimr = DefaultKikimrRunner ();
689+
690+ auto client = kikimr.GetTableClient ();
691+ auto session = client.CreateSession ().GetValueSync ().GetSession ();
692+
693+ // Create table with only NOT NULL fields
694+ const auto queryCreate = Q_ (R"(
695+ --!syntax_v1
696+ CREATE TABLE test_table (
697+ id Uint64 NOT NULL,
698+ value Utf8 NOT NULL,
699+ PRIMARY KEY (id)
700+ );
701+ )" );
702+
703+ auto resultCreate = session.ExecuteSchemeQuery (queryCreate).GetValueSync ();
704+ UNIT_ASSERT_C (resultCreate.IsSuccess (), resultCreate.GetIssues ().ToString ());
705+
706+ {
707+ // Test case from issue #27021
708+ const auto query = Q_ (R"(
709+ --!syntax_v1
710+ DECLARE $data AS List<Struct<id: UInt64, value: Utf8>>;
711+
712+ UPSERT INTO test_table
713+ SELECT * FROM AS_TABLE($data)
714+ RETURNING *;
715+ )" );
716+
717+ auto paramsBuilder = TParamsBuilder ();
718+ auto & dataParam = paramsBuilder.AddParam (" $data" );
719+
720+ dataParam.BeginList ();
721+ dataParam.AddListItem ()
722+ .BeginStruct ()
723+ .AddMember (" id" )
724+ .Uint64 (1 )
725+ .AddMember (" value" )
726+ .Utf8 (" test1" )
727+ .EndStruct ();
728+ dataParam.AddListItem ()
729+ .BeginStruct ()
730+ .AddMember (" id" )
731+ .Uint64 (2 )
732+ .AddMember (" value" )
733+ .Utf8 (" test2" )
734+ .EndStruct ();
735+ dataParam.EndList ();
736+ dataParam.Build ();
737+
738+ auto params = paramsBuilder.Build ();
739+
740+ // This should succeed, but currently fails with infinite loop error
741+ CompareYson (R"( [[1u;"test1"];[2u;"test2"]])" ,
742+ ExecuteReturningQueryWithParams (kikimr, QueryService, query, params));
743+ }
744+
745+ {
746+ // Test with explicit field names in SELECT clause
747+ const auto query = Q_ (R"(
748+ --!syntax_v1
749+ DECLARE $data AS List<Struct<id: UInt64, value: Utf8>>;
750+
751+ UPSERT INTO test_table
752+ SELECT id, value FROM AS_TABLE($data)
753+ RETURNING *;
754+ )" );
755+
756+ auto paramsBuilder = TParamsBuilder ();
757+ auto & dataParam = paramsBuilder.AddParam (" $data" );
758+
759+ dataParam.BeginList ();
760+ dataParam.AddListItem ()
761+ .BeginStruct ()
762+ .AddMember (" id" )
763+ .Uint64 (3 )
764+ .AddMember (" value" )
765+ .Utf8 (" test3" )
766+ .EndStruct ();
767+ dataParam.EndList ();
768+ dataParam.Build ();
769+
770+ auto params = paramsBuilder.Build ();
771+
772+ CompareYson (R"( [[3u;"test3"]])" ,
773+ ExecuteReturningQueryWithParams (kikimr, QueryService, query, params));
774+ }
775+
776+ {
777+ // Test DELETE with RETURNING using AS_TABLE with List parameter (same issue #27021)
778+ // First insert some data without RETURNING
779+ const auto insertQuery = Q_ (R"(
780+ --!syntax_v1
781+ DECLARE $data AS List<Struct<id: UInt64, value: Utf8>>;
782+
783+ UPSERT INTO test_table
784+ SELECT * FROM AS_TABLE($data);
785+ )" );
786+
787+ auto insertParamsBuilder = TParamsBuilder ();
788+ auto & insertDataParam = insertParamsBuilder.AddParam (" $data" );
789+
790+ insertDataParam.BeginList ();
791+ insertDataParam.AddListItem ()
792+ .BeginStruct ()
793+ .AddMember (" id" )
794+ .Uint64 (10 )
795+ .AddMember (" value" )
796+ .Utf8 (" delete1" )
797+ .EndStruct ();
798+ insertDataParam.EndList ();
799+ insertDataParam.Build ();
800+
801+ auto insertParams = insertParamsBuilder.Build ();
802+
803+ // Execute insert without RETURNING
804+ if (QueryService) {
805+ auto qdb = kikimr.GetQueryClient ();
806+ auto qSession = qdb.GetSession ().GetValueSync ().GetSession ();
807+ auto settings = NYdb::NQuery::TExecuteQuerySettings ()
808+ .Syntax (NYdb::NQuery::ESyntax::YqlV1);
809+ auto insertResult = qSession.ExecuteQuery (
810+ insertQuery, NYdb::NQuery::TTxControl::BeginTx ().CommitTx (), insertParams, settings).ExtractValueSync ();
811+ UNIT_ASSERT_VALUES_EQUAL_C (insertResult.GetStatus (), EStatus::SUCCESS, insertResult.GetIssues ().ToString ());
812+ } else {
813+ auto db = kikimr.GetTableClient ();
814+ auto session = db.CreateSession ().GetValueSync ().GetSession ();
815+ auto insertResult = session.ExecuteDataQuery (insertQuery, TTxControl::BeginTx ().CommitTx (), insertParams).ExtractValueSync ();
816+ UNIT_ASSERT_VALUES_EQUAL_C (insertResult.GetStatus (), EStatus::SUCCESS, insertResult.GetIssues ().ToString ());
817+ }
818+
819+ // Now DELETE with RETURNING using AS_TABLE
820+ const auto deleteQuery = Q_ (R"(
821+ --!syntax_v1
822+ DECLARE $data AS List<Struct<id: UInt64>>;
823+
824+ DELETE FROM test_table ON
825+ SELECT * FROM AS_TABLE($data)
826+ RETURNING *;
827+ )" );
828+
829+ auto deleteParamsBuilder = TParamsBuilder ();
830+ auto & deleteDataParam = deleteParamsBuilder.AddParam (" $data" );
831+
832+ deleteDataParam.BeginList ();
833+ deleteDataParam.AddListItem ()
834+ .BeginStruct ()
835+ .AddMember (" id" )
836+ .Uint64 (10 )
837+ .EndStruct ();
838+ deleteDataParam.EndList ();
839+ deleteDataParam.Build ();
840+
841+ auto deleteParams = deleteParamsBuilder.Build ();
842+
843+ // This should succeed, but currently fails with infinite loop error
844+ CompareYson (R"( [[10u;"delete1"]])" ,
845+ ExecuteReturningQueryWithParams (kikimr, QueryService, deleteQuery, deleteParams));
846+ }
847+ }
848+
849+ Y_UNIT_TEST_TWIN (ReturningUpsertAsTableListWithNullable, QueryService) {
850+ // Test that nullable columns work correctly (this should work even before the fix)
851+ auto kikimr = DefaultKikimrRunner ();
852+
853+ auto client = kikimr.GetTableClient ();
854+ auto session = client.CreateSession ().GetValueSync ().GetSession ();
855+
856+ const auto queryCreate = Q_ (R"(
857+ --!syntax_v1
858+ CREATE TABLE test_table_nullable (
859+ id Uint64 NOT NULL,
860+ value Utf8,
861+ PRIMARY KEY (id)
862+ );
863+ )" );
864+
865+ auto resultCreate = session.ExecuteSchemeQuery (queryCreate).GetValueSync ();
866+ UNIT_ASSERT_C (resultCreate.IsSuccess (), resultCreate.GetIssues ().ToString ());
867+
868+ {
869+ const auto query = Q_ (R"(
870+ --!syntax_v1
871+ DECLARE $data AS List<Struct<id: UInt64, value: Utf8?>>;
872+
873+ UPSERT INTO test_table_nullable
874+ SELECT * FROM AS_TABLE($data)
875+ RETURNING *;
876+ )" );
877+
878+ auto paramsBuilder = TParamsBuilder ();
879+ auto & dataParam = paramsBuilder.AddParam (" $data" );
880+
881+ dataParam.BeginList ();
882+ dataParam.AddListItem ()
883+ .BeginStruct ()
884+ .AddMember (" id" )
885+ .Uint64 (1 )
886+ .AddMember (" value" )
887+ .OptionalUtf8 (" test1" )
888+ .EndStruct ();
889+ dataParam.EndList ();
890+ dataParam.Build ();
891+
892+ auto params = paramsBuilder.Build ();
893+
894+ CompareYson (R"( [[1u;["test1"]]])" ,
895+ ExecuteReturningQueryWithParams (kikimr, QueryService, query, params));
896+ }
897+
898+ {
899+ // Test DELETE with RETURNING using AS_TABLE with List parameter (with nullable columns)
900+ // First insert some data without RETURNING
901+ const auto insertQuery = Q_ (R"(
902+ --!syntax_v1
903+ DECLARE $data AS List<Struct<id: UInt64, value: Utf8?>>;
904+
905+ UPSERT INTO test_table_nullable
906+ SELECT * FROM AS_TABLE($data);
907+ )" );
908+
909+ auto insertParamsBuilder = TParamsBuilder ();
910+ auto & insertDataParam = insertParamsBuilder.AddParam (" $data" );
911+
912+ insertDataParam.BeginList ();
913+ insertDataParam.AddListItem ()
914+ .BeginStruct ()
915+ .AddMember (" id" )
916+ .Uint64 (20 )
917+ .AddMember (" value" )
918+ .OptionalUtf8 (" delete_nullable1" )
919+ .EndStruct ();
920+ insertDataParam.EndList ();
921+ insertDataParam.Build ();
922+
923+ auto insertParams = insertParamsBuilder.Build ();
924+
925+ // Execute insert without RETURNING
926+ if (QueryService) {
927+ auto qdb = kikimr.GetQueryClient ();
928+ auto qSession = qdb.GetSession ().GetValueSync ().GetSession ();
929+ auto settings = NYdb::NQuery::TExecuteQuerySettings ()
930+ .Syntax (NYdb::NQuery::ESyntax::YqlV1);
931+ auto insertResult = qSession.ExecuteQuery (
932+ insertQuery, NYdb::NQuery::TTxControl::BeginTx ().CommitTx (), insertParams, settings).ExtractValueSync ();
933+ UNIT_ASSERT_VALUES_EQUAL_C (insertResult.GetStatus (), EStatus::SUCCESS, insertResult.GetIssues ().ToString ());
934+ } else {
935+ auto db = kikimr.GetTableClient ();
936+ auto session = db.CreateSession ().GetValueSync ().GetSession ();
937+ auto insertResult = session.ExecuteDataQuery (insertQuery, TTxControl::BeginTx ().CommitTx (), insertParams).ExtractValueSync ();
938+ UNIT_ASSERT_VALUES_EQUAL_C (insertResult.GetStatus (), EStatus::SUCCESS, insertResult.GetIssues ().ToString ());
939+ }
940+
941+ // Now DELETE with RETURNING using AS_TABLE
942+ const auto deleteQuery = Q_ (R"(
943+ --!syntax_v1
944+ DECLARE $data AS List<Struct<id: UInt64>>;
945+
946+ DELETE FROM test_table_nullable ON
947+ SELECT * FROM AS_TABLE($data)
948+ RETURNING *;
949+ )" );
950+
951+ auto deleteParamsBuilder = TParamsBuilder ();
952+ auto & deleteDataParam = deleteParamsBuilder.AddParam (" $data" );
953+
954+ deleteDataParam.BeginList ();
955+ deleteDataParam.AddListItem ()
956+ .BeginStruct ()
957+ .AddMember (" id" )
958+ .Uint64 (20 )
959+ .EndStruct ();
960+ deleteDataParam.EndList ();
961+ deleteDataParam.Build ();
962+
963+ auto deleteParams = deleteParamsBuilder.Build ();
964+
965+ CompareYson (R"( [[20u;["delete_nullable1"]]])" ,
966+ ExecuteReturningQueryWithParams (kikimr, QueryService, deleteQuery, deleteParams));
967+ }
968+ }
969+
666970}
667971
668972} // namespace NKikimr::NKqp
0 commit comments