@@ -612,4 +612,99 @@ TEST_F(TableFunctionTest, vjson_each_get_same_many_values) {
612612 fn.process_close ();
613613 }
614614}
615+
616+ TEST_F (TableFunctionTest, vjson_each_outer) {
617+ init_expr_context (1 );
618+ VJsonEachTableFn fn;
619+ fn.set_expr_context (_ctx);
620+
621+ // set_outer() correctly sets the is_outer flag
622+ EXPECT_FALSE (fn.is_outer ());
623+ fn.set_outer ();
624+ EXPECT_TRUE (fn.is_outer ());
625+
626+ // Normal object: outer flag does not affect KV expansion
627+ {
628+ auto block = build_jsonb_input_block ({{R"( {"a":"foo","b":123})" }});
629+ auto rows = run_json_each_fn (&fn, block.get (), true );
630+ ASSERT_EQ (2u , rows.size ());
631+ EXPECT_EQ (" a" , rows[0 ].first );
632+ EXPECT_EQ (" \" foo\" " , rows[0 ].second );
633+ EXPECT_EQ (" b" , rows[1 ].first );
634+ EXPECT_EQ (" 123" , rows[1 ].second );
635+ }
636+
637+ // For NULL / empty-object / non-object inputs: current_empty() is true.
638+ // The operator calls get_value() unconditionally when is_outer() — verify that
639+ // get_value() inserts exactly one default (NULL) struct row in each case.
640+ DataTypePtr key_dt = make_nullable (DataTypeFactory::instance ().create_data_type (
641+ doris::PrimitiveType::TYPE_VARCHAR, false ));
642+ DataTypePtr val_dt = make_nullable (
643+ DataTypeFactory::instance ().create_data_type (doris::PrimitiveType::TYPE_JSONB, false ));
644+ DataTypePtr struct_dt =
645+ make_nullable (std::make_shared<DataTypeStruct>(DataTypes {key_dt, val_dt}));
646+
647+ TQueryOptions q_opts;
648+ TQueryGlobals q_globals;
649+ RuntimeState rs (q_opts, q_globals);
650+
651+ for (const char * input : {" " , " {}" , " [1,2,3]" }) {
652+ auto block = build_jsonb_input_block ({{input}});
653+ ASSERT_TRUE (fn.process_init (block.get (), &rs).ok ()) << " input: " << input;
654+ fn.process_row (0 );
655+ EXPECT_TRUE (fn.current_empty ()) << " input: " << input;
656+
657+ auto out_col = struct_dt->create_column ();
658+ fn.get_value (out_col, 1 );
659+ ASSERT_EQ (1u , out_col->size ()) << " input: " << input;
660+ EXPECT_TRUE (out_col->is_null_at (0 )) << " input: " << input;
661+ fn.process_close ();
662+ }
663+ }
664+
665+ TEST_F (TableFunctionTest, vjson_each_text_outer) {
666+ init_expr_context (1 );
667+ VJsonEachTextTableFn fn;
668+ fn.set_expr_context (_ctx);
669+
670+ EXPECT_FALSE (fn.is_outer ());
671+ fn.set_outer ();
672+ EXPECT_TRUE (fn.is_outer ());
673+
674+ // Normal object: text mode (strings unquoted), outer flag does not affect expansion
675+ {
676+ auto block = build_jsonb_input_block ({{R"( {"a":"foo","b":123})" }});
677+ auto rows = run_json_each_fn (&fn, block.get (), false );
678+ ASSERT_EQ (2u , rows.size ());
679+ EXPECT_EQ (" a" , rows[0 ].first );
680+ EXPECT_EQ (" foo" , rows[0 ].second );
681+ EXPECT_EQ (" b" , rows[1 ].first );
682+ EXPECT_EQ (" 123" , rows[1 ].second );
683+ }
684+
685+ // NULL / empty-object / non-object → current_empty(), get_value() inserts one default row
686+ DataTypePtr key_dt = make_nullable (DataTypeFactory::instance ().create_data_type (
687+ doris::PrimitiveType::TYPE_VARCHAR, false ));
688+ DataTypePtr val_dt = make_nullable (DataTypeFactory::instance ().create_data_type (
689+ doris::PrimitiveType::TYPE_VARCHAR, false ));
690+ DataTypePtr struct_dt =
691+ make_nullable (std::make_shared<DataTypeStruct>(DataTypes {key_dt, val_dt}));
692+
693+ TQueryOptions q_opts;
694+ TQueryGlobals q_globals;
695+ RuntimeState rs (q_opts, q_globals);
696+
697+ for (const char * input : {" " , " {}" , " [1,2,3]" }) {
698+ auto block = build_jsonb_input_block ({{input}});
699+ ASSERT_TRUE (fn.process_init (block.get (), &rs).ok ()) << " input: " << input;
700+ fn.process_row (0 );
701+ EXPECT_TRUE (fn.current_empty ()) << " input: " << input;
702+
703+ auto out_col = struct_dt->create_column ();
704+ fn.get_value (out_col, 1 );
705+ ASSERT_EQ (1u , out_col->size ()) << " input: " << input;
706+ EXPECT_TRUE (out_col->is_null_at (0 )) << " input: " << input;
707+ fn.process_close ();
708+ }
709+ }
615710} // namespace doris::vectorized
0 commit comments