This is an automated email from the ASF dual-hosted git repository.

zclll pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new b34106dcdf7 [Feature](func) Support table function json_each, 
json_each_text (#60910)
b34106dcdf7 is described below

commit b34106dcdf709f923275a753073acd23401162ce
Author: linrrarity <[email protected]>
AuthorDate: Fri Mar 27 11:05:21 2026 +0800

    [Feature](func) Support table function json_each, json_each_text (#60910)
    
    doc: https://github.com/apache/doris-website/pull/3422
    
    ```text
    Doris> SELECT k, v
        -> FROM (SELECT 1) dummy
        -> LATERAL VIEW json_each('{"a":"foo","b":"bar"}') t AS k, v;
    +------+-------+
    | k    | v     |
    +------+-------+
    | a    | "foo" |
    | b    | "bar" |
    +------+-------+
    2 rows in set (0.01 sec)
    
    Doris> SELECT k, v
        -> FROM (SELECT 1) dummy
        -> LATERAL VIEW json_each_text('{"a":"foo","b":"bar"}') t AS k, v;
    +------+------+
    | k    | v    |
    +------+------+
    | a    | foo  |
    | b    | bar  |
    +------+------+
    2 rows in set (0.01 sec)
    ```
---
 be/src/exprs/function/function_fake.cpp            |   40 +-
 .../table_function/table_function_factory.cpp      |    3 +
 be/src/exprs/table_function/vjson_each.cpp         |  207 ++++
 be/src/exprs/table_function/vjson_each.h           |   74 ++
 be/test/exprs/function/table_function_test.cpp     | 1038 ++++++++++++++++++++
 .../catalog/BuiltinTableGeneratingFunctions.java   |   10 +
 .../expressions/functions/generator/JsonEach.java  |   79 ++
 .../functions/generator/JsonEachOuter.java         |   72 ++
 .../functions/generator/JsonEachText.java          |   80 ++
 .../functions/generator/JsonEachTextOuter.java     |   73 ++
 .../visitor/TableGeneratingFunctionVisitor.java    |   20 +
 .../sql_functions/table_function/json_each.out     |  174 ++++
 .../sql_functions/table_function/json_each.groovy  |  419 ++++++++
 13 files changed, 2281 insertions(+), 8 deletions(-)

diff --git a/be/src/exprs/function/function_fake.cpp 
b/be/src/exprs/function/function_fake.cpp
index 5a5b5735096..0966ed9af61 100644
--- a/be/src/exprs/function/function_fake.cpp
+++ b/be/src/exprs/function/function_fake.cpp
@@ -60,8 +60,7 @@ struct FunctionFakeBaseImpl {
 
 struct FunctionExplode {
     static DataTypePtr get_return_type_impl(const DataTypes& arguments) {
-        DCHECK(arguments[0]->get_primitive_type() == TYPE_ARRAY)
-                << arguments[0]->get_name() << " not supported";
+        DORIS_CHECK(arguments[0]->get_primitive_type() == TYPE_ARRAY);
         return make_nullable(
                 
check_and_get_data_type<DataTypeArray>(arguments[0].get())->get_nested_type());
     }
@@ -103,8 +102,7 @@ struct FunctionExplodeV2 {
 // explode map: make map k,v as struct field
 struct FunctionExplodeMap {
     static DataTypePtr get_return_type_impl(const DataTypes& arguments) {
-        DCHECK(arguments[0]->get_primitive_type() == TYPE_MAP)
-                << arguments[0]->get_name() << " not supported";
+        DORIS_CHECK(arguments[0]->get_primitive_type() == TYPE_MAP);
         DataTypes fieldTypes(2);
         fieldTypes[0] = 
check_and_get_data_type<DataTypeMap>(arguments[0].get())->get_key_type();
         fieldTypes[1] = 
check_and_get_data_type<DataTypeMap>(arguments[0].get())->get_value_type();
@@ -120,8 +118,7 @@ struct FunctionPoseExplode {
         DataTypes fieldTypes(arguments.size() + 1);
         fieldTypes[0] = std::make_shared<DataTypeInt32>();
         for (int i = 0; i < arguments.size(); i++) {
-            DCHECK_EQ(arguments[i]->get_primitive_type(), TYPE_ARRAY)
-                    << arguments[i]->get_name() << " not supported";
+            DORIS_CHECK(arguments[i]->get_primitive_type() == TYPE_ARRAY);
             auto nestedType =
                     
check_and_get_data_type<DataTypeArray>(arguments[i].get())->get_nested_type();
             fieldTypes[i + 1] = make_nullable(nestedType);
@@ -140,8 +137,7 @@ struct FunctionPoseExplode {
 // explode json-object: expands json-object to struct with a pair of key and 
value in column string
 struct FunctionExplodeJsonObject {
     static DataTypePtr get_return_type_impl(const DataTypes& arguments) {
-        DCHECK_EQ(arguments[0]->get_primitive_type(), 
PrimitiveType::TYPE_JSONB)
-                << " explode json object " << arguments[0]->get_name() << " 
not supported";
+        DORIS_CHECK(arguments[0]->get_primitive_type() == 
PrimitiveType::TYPE_JSONB);
         DataTypes fieldTypes(2);
         fieldTypes[0] = make_nullable(std::make_shared<DataTypeString>());
         fieldTypes[1] = make_nullable(std::make_shared<DataTypeJsonb>());
@@ -151,6 +147,32 @@ struct FunctionExplodeJsonObject {
     static std::string get_error_msg() { return "Fake function do not support 
execute"; }
 };
 
+// json_each(json) -> Nullable(Struct(key Nullable(String), value 
Nullable(JSONB)))
+struct FunctionJsonEach {
+    static DataTypePtr get_return_type_impl(const DataTypes& arguments) {
+        DORIS_CHECK(arguments[0]->get_primitive_type() == 
PrimitiveType::TYPE_JSONB);
+        DataTypes fieldTypes(2);
+        fieldTypes[0] = make_nullable(std::make_shared<DataTypeString>());
+        fieldTypes[1] = make_nullable(std::make_shared<DataTypeJsonb>());
+        return make_nullable(std::make_shared<DataTypeStruct>(fieldTypes));
+    }
+    static DataTypes get_variadic_argument_types() { return {}; }
+    static std::string get_error_msg() { return "Fake function do not support 
execute"; }
+};
+
+// json_each_text(json) -> Nullable(Struct(key Nullable(String), value 
Nullable(String)))
+struct FunctionJsonEachText {
+    static DataTypePtr get_return_type_impl(const DataTypes& arguments) {
+        DORIS_CHECK(arguments[0]->get_primitive_type() == 
PrimitiveType::TYPE_JSONB);
+        DataTypes fieldTypes(2);
+        fieldTypes[0] = make_nullable(std::make_shared<DataTypeString>());
+        fieldTypes[1] = make_nullable(std::make_shared<DataTypeString>());
+        return make_nullable(std::make_shared<DataTypeStruct>(fieldTypes));
+    }
+    static DataTypes get_variadic_argument_types() { return {}; }
+    static std::string get_error_msg() { return "Fake function do not support 
execute"; }
+};
+
 struct FunctionEsquery {
     static DataTypePtr get_return_type_impl(const DataTypes& arguments) {
         return 
FunctionFakeBaseImpl<DataTypeUInt8>::get_return_type_impl(arguments);
@@ -239,6 +261,8 @@ void register_function_fake(SimpleFunctionFactory& factory) 
{
     register_table_function_expand_outer<FunctionExplodeMap>(factory, 
"explode_map");
 
     register_table_function_expand_outer<FunctionExplodeJsonObject>(factory, 
"explode_json_object");
+    register_table_function_expand_outer<FunctionJsonEach>(factory, 
"json_each");
+    register_table_function_expand_outer<FunctionJsonEachText>(factory, 
"json_each_text");
     register_table_function_expand_outer_default<DataTypeString, 
false>(factory, "explode_split");
     register_table_function_expand_outer_default<DataTypeInt32, 
false>(factory, "explode_numbers");
     register_table_function_expand_outer_default<DataTypeInt64, false>(factory,
diff --git a/be/src/exprs/table_function/table_function_factory.cpp 
b/be/src/exprs/table_function/table_function_factory.cpp
index ffe4b8fddd0..4e64fdab1e7 100644
--- a/be/src/exprs/table_function/table_function_factory.cpp
+++ b/be/src/exprs/table_function/table_function_factory.cpp
@@ -34,6 +34,7 @@
 #include "exprs/table_function/vexplode_map.h"
 #include "exprs/table_function/vexplode_numbers.h"
 #include "exprs/table_function/vexplode_v2.h"
+#include "exprs/table_function/vjson_each.h"
 
 namespace doris {
 #include "common/compile_check_begin.h"
@@ -50,6 +51,8 @@ const std::unordered_map<std::string, 
std::function<std::unique_ptr<TableFunctio
                 {"explode_bitmap", 
TableFunctionCreator<VExplodeBitmapTableFunction>()},
                 {"explode_map", TableFunctionCreator<VExplodeMapTableFunction> 
{}},
                 {"explode_json_object", 
TableFunctionCreator<VExplodeJsonObjectTableFunction> {}},
+                {"json_each", TableFunctionCreator<VJsonEachTableFn> {}},
+                {"json_each_text", TableFunctionCreator<VJsonEachTextTableFn> 
{}},
                 {"posexplode", TableFunctionCreator<VExplodeV2TableFunction> 
{}},
                 {"explode", TableFunctionCreator<VExplodeV2TableFunction> {}},
                 {"explode_variant_array_old", 
TableFunctionCreator<VExplodeTableFunction>()},
diff --git a/be/src/exprs/table_function/vjson_each.cpp 
b/be/src/exprs/table_function/vjson_each.cpp
new file mode 100644
index 00000000000..9f42535c7f4
--- /dev/null
+++ b/be/src/exprs/table_function/vjson_each.cpp
@@ -0,0 +1,207 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "exprs/table_function/vjson_each.h"
+
+#include <glog/logging.h>
+
+#include <ostream>
+#include <string>
+
+#include "common/status.h"
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/block/column_with_type_and_name.h"
+#include "core/column/column.h"
+#include "core/column/column_const.h"
+#include "core/column/column_struct.h"
+#include "core/string_ref.h"
+#include "exprs/vexpr.h"
+#include "exprs/vexpr_context.h"
+#include "util/jsonb_document.h"
+#include "util/jsonb_utils.h"
+#include "util/jsonb_writer.h"
+
+namespace doris {
+#include "common/compile_check_begin.h"
+
+template <bool TEXT_MODE>
+VJsonEachTableFunction<TEXT_MODE>::VJsonEachTableFunction() {
+    _fn_name = TEXT_MODE ? "vjson_each_text" : "vjson_each";
+}
+
+template <bool TEXT_MODE>
+Status VJsonEachTableFunction<TEXT_MODE>::process_init(Block* block, 
RuntimeState* /*state*/) {
+    int value_column_idx = -1;
+    
RETURN_IF_ERROR(_expr_context->root()->children()[0]->execute(_expr_context.get(),
 block,
+                                                                  
&value_column_idx));
+    auto [col, is_const] = 
unpack_if_const(block->get_by_position(value_column_idx).column);
+    _json_column = col;
+    _is_const = is_const;
+    return Status::OK();
+}
+
+// Helper: insert one JsonbValue as plain text into a 
ColumnNullable<ColumnString>.
+// For strings: raw blob content (quotes stripped, matching json_each_text PG 
semantics).
+// For null JSON values: SQL NULL (insert_default).
+// For all others (numbers, bools, objects, arrays): JSON text representation.
+static void insert_value_as_text(const JsonbValue* value, MutableColumnPtr& 
col) {
+    if (value == nullptr || value->isNull()) {
+        col->insert_default();
+        return;
+    }
+    if (value->isString()) {
+        const auto* str_val = value->unpack<JsonbStringVal>();
+        col->insert_data(str_val->getBlob(), str_val->getBlobLen());
+    } else {
+        JsonbToJson converter;
+        std::string text = converter.to_json_string(value);
+        col->insert_data(text.data(), text.size());
+    }
+}
+
+// Helper: insert one JsonbValue in JSONB binary form into a 
ColumnNullable<ColumnString>.
+// For null JSON values: SQL NULL (insert_default).
+// For all others: write JSONB binary via JsonbWriter.
+static void insert_value_as_json(const JsonbValue* value, MutableColumnPtr& 
col,
+                                 JsonbWriter& writer) {
+    if (value == nullptr || value->isNull()) {
+        col->insert_default();
+        return;
+    }
+    writer.reset();
+    writer.writeValue(value);
+    const auto* buf = writer.getOutput()->getBuffer();
+    size_t len = writer.getOutput()->getSize();
+    col->insert_data(buf, len);
+}
+
+template <bool TEXT_MODE>
+void VJsonEachTableFunction<TEXT_MODE>::process_row(size_t row_idx) {
+    TableFunction::process_row(row_idx);
+    if (_is_const && _cur_size > 0) {
+        return;
+    }
+
+    StringRef text;
+    const size_t idx = _is_const ? 0 : row_idx;
+    if (const auto* nullable_col = 
check_and_get_column<ColumnNullable>(*_json_column)) {
+        if (nullable_col->is_null_at(idx)) {
+            return;
+        }
+        text = assert_cast<const 
ColumnString&>(nullable_col->get_nested_column()).get_data_at(idx);
+    } else {
+        text = assert_cast<const 
ColumnString&>(*_json_column).get_data_at(idx);
+    }
+
+    const JsonbDocument* doc = nullptr;
+    auto st = JsonbDocument::checkAndCreateDocument(text.data, text.size, 
&doc);
+    if (!st.ok() || !doc || !doc->getValue()) [[unlikely]] {
+        return;
+    }
+
+    const JsonbValue* jv = doc->getValue();
+    if (!jv->isObject()) {
+        return;
+    }
+
+    const auto* obj = jv->unpack<ObjectVal>();
+    _cur_size = obj->numElem();
+    if (_cur_size == 0) {
+        return;
+    }
+
+    _kv_pairs.first = ColumnNullable::create(ColumnString::create(), 
ColumnUInt8::create());
+    _kv_pairs.second = ColumnNullable::create(ColumnString::create(), 
ColumnUInt8::create());
+    _kv_pairs.first->reserve(_cur_size);
+    _kv_pairs.second->reserve(_cur_size);
+
+    if constexpr (TEXT_MODE) {
+        for (const auto& kv : *obj) {
+            _kv_pairs.first->insert_data(kv.getKeyStr(), kv.klen());
+            insert_value_as_text(kv.value(), _kv_pairs.second);
+        }
+    } else {
+        JsonbWriter writer;
+        for (const auto& kv : *obj) {
+            _kv_pairs.first->insert_data(kv.getKeyStr(), kv.klen());
+            insert_value_as_json(kv.value(), _kv_pairs.second, writer);
+        }
+    }
+}
+
+template <bool TEXT_MODE>
+void VJsonEachTableFunction<TEXT_MODE>::process_close() {
+    _json_column = nullptr;
+    _kv_pairs.first = nullptr;
+    _kv_pairs.second = nullptr;
+    _cur_size = 0;
+}
+
+template <bool TEXT_MODE>
+void VJsonEachTableFunction<TEXT_MODE>::get_same_many_values(MutableColumnPtr& 
column, int length) {
+    if (current_empty()) {
+        column->insert_many_defaults(length);
+        return;
+    }
+
+    ColumnStruct* ret;
+    if (_is_nullable) {
+        auto* nullable = assert_cast<ColumnNullable*>(column.get());
+        ret = 
assert_cast<ColumnStruct*>(nullable->get_nested_column_ptr().get());
+        assert_cast<ColumnUInt8*>(nullable->get_null_map_column_ptr().get())
+                ->insert_many_defaults(length);
+    } else {
+        ret = assert_cast<ColumnStruct*>(column.get());
+    }
+
+    ret->get_column(0).insert_many_from(*_kv_pairs.first, _cur_offset, length);
+    ret->get_column(1).insert_many_from(*_kv_pairs.second, _cur_offset, 
length);
+}
+
+template <bool TEXT_MODE>
+int VJsonEachTableFunction<TEXT_MODE>::get_value(MutableColumnPtr& column, int 
max_step) {
+    max_step = std::min(max_step, (int)(_cur_size - _cur_offset));
+
+    if (current_empty()) {
+        column->insert_default();
+        max_step = 1;
+    } else {
+        ColumnStruct* struct_col = nullptr;
+        if (_is_nullable) {
+            auto* nullable_col = assert_cast<ColumnNullable*>(column.get());
+            struct_col = 
assert_cast<ColumnStruct*>(nullable_col->get_nested_column_ptr().get());
+            
assert_cast<ColumnUInt8*>(nullable_col->get_null_map_column_ptr().get())
+                    ->insert_many_defaults(max_step);
+        } else {
+            struct_col = assert_cast<ColumnStruct*>(column.get());
+        }
+
+        struct_col->get_column(0).insert_range_from(*_kv_pairs.first, 
_cur_offset, max_step);
+        struct_col->get_column(1).insert_range_from(*_kv_pairs.second, 
_cur_offset, max_step);
+    }
+
+    forward(max_step);
+    return max_step;
+}
+
+// // Explicit template instantiations
+template class VJsonEachTableFunction<false>; // json_each
+template class VJsonEachTableFunction<true>;  // json_each_text
+
+#include "common/compile_check_end.h"
+} // namespace doris
diff --git a/be/src/exprs/table_function/vjson_each.h 
b/be/src/exprs/table_function/vjson_each.h
new file mode 100644
index 00000000000..830e44d3058
--- /dev/null
+++ b/be/src/exprs/table_function/vjson_each.h
@@ -0,0 +1,74 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <cstddef>
+
+#include "common/status.h"
+#include "core/data_type/data_type.h"
+#include "exprs/table_function/table_function.h"
+
+namespace doris {
+#include "common/compile_check_begin.h"
+class Block;
+
+// json_each('{"a":"foo","b":123}') →
+// | key | value        |
+// | a   | "foo" (JSON) |
+// | b   | 123   (JSON) |
+//
+// json_each_text('{"a":"foo","b":123}') →
+// | key | value   |
+// | a   | foo     |  ← string unquoted
+// | b   | 123     |  ← number as text
+//
+// TEXT_MODE=false → json_each  (value column type: JSONB binary)
+// TEXT_MODE=true  → json_each_text (value column type: plain STRING)
+template <bool TEXT_MODE>
+class VJsonEachTableFunction : public TableFunction {
+    ENABLE_FACTORY_CREATOR(VJsonEachTableFunction);
+
+public:
+    VJsonEachTableFunction();
+
+    ~VJsonEachTableFunction() override = default;
+
+    Status process_init(Block* block, RuntimeState* state) override;
+    void process_row(size_t row_idx) override;
+    void process_close() override;
+    void get_same_many_values(MutableColumnPtr& column, int length) override;
+    int get_value(MutableColumnPtr& column, int max_step) override;
+
+#ifdef BE_TEST
+    const ColumnPtr& test_json_column() const { return _json_column; }
+    const MutableColumnPtr& test_kv_pairs_first() const { return 
_kv_pairs.first; }
+    const MutableColumnPtr& test_kv_pairs_second() const { return 
_kv_pairs.second; }
+#endif
+
+private:
+    ColumnPtr _json_column;
+    // _kv_pairs.first  : ColumnNullable<ColumnString>  key (always plain text)
+    // _kv_pairs.second : ColumnNullable<ColumnString>  value (JSONB bytes or 
plain text)
+    std::pair<MutableColumnPtr, MutableColumnPtr> _kv_pairs;
+};
+
+using VJsonEachTableFn = VJsonEachTableFunction<false>;
+using VJsonEachTextTableFn = VJsonEachTableFunction<true>;
+
+#include "common/compile_check_end.h"
+} // namespace doris
diff --git a/be/test/exprs/function/table_function_test.cpp 
b/be/test/exprs/function/table_function_test.cpp
index 09056d829a9..5ae08c411ac 100644
--- a/be/test/exprs/function/table_function_test.cpp
+++ b/be/test/exprs/function/table_function_test.cpp
@@ -30,7 +30,11 @@
 #include "exprs/table_function/vexplode.h"
 #include "exprs/table_function/vexplode_numbers.h"
 #include "exprs/table_function/vexplode_v2.h"
+#include "exprs/table_function/vjson_each.h"
 #include "testutil/any_type.h"
+#include "util/jsonb_parser_simd.h"
+#include "util/jsonb_utils.h"
+#include "util/jsonb_writer.h"
 
 namespace doris {
 
@@ -308,4 +312,1038 @@ TEST_F(TableFunctionTest, vexplode_numbers) {
     }
 }
 
+// ---------------------------------------------------------------------------
+// Direct-API helpers for json_each / json_each_text tests.
+// The test framework's check_vec_table_function does not properly support
+// TYPE_STRUCT output (insert_cell always expects ColumnNullable wrapping the
+// struct column), so we drive the table function API directly.
+// ---------------------------------------------------------------------------
+
+// Build a one-column JSONB input block.  An empty string means SQL NULL.
+static std::unique_ptr<Block> build_jsonb_input_block(const 
std::vector<std::string>& json_rows) {
+    auto str_col = ColumnString::create();
+    auto null_col = ColumnUInt8::create();
+    for (const auto& json : json_rows) {
+        if (json.empty()) {
+            str_col->insert_default();
+            null_col->insert_value(1);
+        } else {
+            JsonbWriter writer;
+            if (JsonbParser::parse(json.c_str(), json.size(), writer).ok()) {
+                str_col->insert_data(writer.getOutput()->getBuffer(),
+                                     writer.getOutput()->getSize());
+                null_col->insert_value(0);
+            } else {
+                str_col->insert_default();
+                null_col->insert_value(1);
+            }
+        }
+    }
+    auto col = ColumnNullable::create(std::move(str_col), std::move(null_col));
+    auto block = Block::create_unique();
+    block->insert({std::move(col),
+                   make_nullable(DataTypeFactory::instance().create_data_type(
+                           doris::PrimitiveType::TYPE_JSONB, false)),
+                   "jval"});
+    return block;
+}
+
+// Run the given table function over all rows in block.
+// Returns list of (key, value) pairs where value == "__NULL__" means SQL NULL.
+// val_is_jsonb controls whether the value column is decoded as JSONB→JSON 
text or plain text.
+static std::vector<std::pair<std::string, std::string>> 
run_json_each_fn(TableFunction* fn,
+                                                                         
Block* block,
+                                                                         bool 
val_is_jsonb) {
+    // Output type: Nullable(Struct(Nullable(VARCHAR key), 
Nullable(VARCHAR/JSONB value)))
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            val_is_jsonb ? doris::PrimitiveType::TYPE_JSONB : 
doris::PrimitiveType::TYPE_VARCHAR,
+            false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    auto out_col = struct_dt->create_column();
+    fn->set_nullable();
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+    EXPECT_TRUE(fn->process_init(block, &rs).ok());
+
+    for (size_t row = 0; row < block->rows(); ++row) {
+        fn->process_row(row);
+        if (!fn->current_empty()) {
+            do {
+                fn->get_value(out_col, 1);
+            } while (!fn->eos());
+        }
+    }
+    fn->process_close();
+
+    std::vector<std::pair<std::string, std::string>> result;
+    const auto& nullable_out = assert_cast<const ColumnNullable&>(*out_col);
+    const auto& struct_col = assert_cast<const 
ColumnStruct&>(nullable_out.get_nested_column());
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(0));
+    const auto& val_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(1));
+
+    for (size_t i = 0; i < struct_col.size(); ++i) {
+        if (nullable_out.is_null_at(i)) {
+            result.emplace_back("__NULL_ROW__", "__NULL_ROW__");
+            continue;
+        }
+        std::string key;
+        if (!key_col.is_null_at(i)) {
+            StringRef sr = key_col.get_nested_column().get_data_at(i);
+            key.assign(sr.data, sr.size);
+        }
+        std::string val;
+        if (val_col.is_null_at(i)) {
+            val = "__NULL__";
+        } else {
+            StringRef sr = val_col.get_nested_column().get_data_at(i);
+            if (val_is_jsonb) {
+                // JSONB binary → JSON text for comparison
+                const JsonbDocument* doc = nullptr;
+                if (JsonbDocument::checkAndCreateDocument(sr.data, sr.size, 
&doc).ok() && doc &&
+                    doc->getValue()) {
+                    JsonbToJson converter;
+                    val = converter.to_json_string(doc->getValue());
+                } else {
+                    val = "__BAD_JSONB__";
+                }
+            } else {
+                val.assign(sr.data, sr.size);
+            }
+        }
+        result.emplace_back(std::move(key), std::move(val));
+    }
+    return result;
+}
+
+TEST_F(TableFunctionTest, vjson_each) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    // basic: string and numeric values; JSONB value column shows JSON text 
with quotes
+    {
+        auto block = build_jsonb_input_block({{R"({"a":"foo","b":123})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        ASSERT_EQ(2u, rows.size());
+        EXPECT_EQ("a", rows[0].first);
+        EXPECT_EQ("\"foo\"", rows[0].second); // JSONB string → JSON text 
includes quotes
+        EXPECT_EQ("b", rows[1].first);
+        EXPECT_EQ("123", rows[1].second);
+    }
+
+    // JSON null value → SQL NULL
+    {
+        auto block = build_jsonb_input_block({{R"({"x":null})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        ASSERT_EQ(1u, rows.size());
+        EXPECT_EQ("x", rows[0].first);
+        EXPECT_EQ("__NULL__", rows[0].second);
+    }
+
+    // boolean and negative int
+    {
+        auto block = build_jsonb_input_block({{R"({"flag":true,"neg":-1})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        ASSERT_EQ(2u, rows.size());
+        bool ok_flag = false, ok_neg = false;
+        for (auto& kv : rows) {
+            if (kv.first == "flag") {
+                EXPECT_EQ("true", kv.second);
+                ok_flag = true;
+            }
+            if (kv.first == "neg") {
+                EXPECT_EQ("-1", kv.second);
+                ok_neg = true;
+            }
+        }
+        EXPECT_TRUE(ok_flag) << "key 'flag' not found";
+        EXPECT_TRUE(ok_neg) << "key 'neg' not found";
+    }
+
+    // SQL NULL input → 0 rows
+    {
+        auto block = build_jsonb_input_block({{""}}); // empty string → SQL 
NULL
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        EXPECT_EQ(0u, rows.size());
+    }
+
+    // empty object → 0 rows
+    {
+        auto block = build_jsonb_input_block({{"{}"}});
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        EXPECT_EQ(0u, rows.size());
+    }
+
+    // non-object input → 0 rows
+    {
+        auto block = build_jsonb_input_block({{"[1,2,3]"}});
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        EXPECT_EQ(0u, rows.size());
+    }
+}
+
+TEST_F(TableFunctionTest, vjson_each_text) {
+    init_expr_context(1);
+    VJsonEachTextTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    // basic: strings unquoted (text mode), numbers as plain text
+    {
+        auto block = build_jsonb_input_block({{R"({"a":"foo","b":123})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), false);
+        ASSERT_EQ(2u, rows.size());
+        EXPECT_EQ("a", rows[0].first);
+        EXPECT_EQ("foo", rows[0].second); // string unquoted in text mode
+        EXPECT_EQ("b", rows[1].first);
+        EXPECT_EQ("123", rows[1].second);
+    }
+
+    // booleans
+    {
+        auto block = build_jsonb_input_block({{R"({"t":true,"f":false})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), false);
+        ASSERT_EQ(2u, rows.size());
+        bool ok_t = false, ok_f = false;
+        for (auto& kv : rows) {
+            if (kv.first == "t") {
+                EXPECT_EQ("true", kv.second);
+                ok_t = true;
+            }
+            if (kv.first == "f") {
+                EXPECT_EQ("false", kv.second);
+                ok_f = true;
+            }
+        }
+        EXPECT_TRUE(ok_t) << "key 't' not found";
+        EXPECT_TRUE(ok_f) << "key 'f' not found";
+    }
+
+    // JSON null → SQL NULL
+    {
+        auto block = build_jsonb_input_block({{R"({"x":null})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), false);
+        ASSERT_EQ(1u, rows.size());
+        EXPECT_EQ("x", rows[0].first);
+        EXPECT_EQ("__NULL__", rows[0].second);
+    }
+
+    // SQL NULL input → 0 rows
+    {
+        auto block = build_jsonb_input_block({{""}});
+        auto rows = run_json_each_fn(&fn, block.get(), false);
+        EXPECT_EQ(0u, rows.size());
+    }
+
+    // empty object → 0 rows
+    {
+        auto block = build_jsonb_input_block({{"{}"}});
+        auto rows = run_json_each_fn(&fn, block.get(), false);
+        EXPECT_EQ(0u, rows.size());
+    }
+}
+TEST_F(TableFunctionTest, vjson_each_get_same_many_values) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    // Case 1: normal object — get_same_many_values replicates the entry at 
_cur_offset.
+    // Simulates a non-last table function being asked to copy its current 
value 3 times
+    // to match 3 rows emitted by the driving (last) function in the same pass.
+    {
+        auto block = build_jsonb_input_block({{R"({"k0":"v0","k1":"v1"})"}});
+        ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+        fn.process_row(0);
+        ASSERT_FALSE(fn.current_empty());
+
+        auto out_col = struct_dt->create_column();
+        fn.get_same_many_values(out_col, 3);
+
+        const auto& nullable_out = assert_cast<const 
ColumnNullable&>(*out_col);
+        ASSERT_EQ(3u, nullable_out.size());
+        const auto& struct_col = assert_cast<const 
ColumnStruct&>(nullable_out.get_nested_column());
+        const auto& key_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(0));
+        // All 3 output rows should carry the entry at _cur_offset=0 ("k0")
+        for (size_t i = 0; i < 3; ++i) {
+            EXPECT_FALSE(nullable_out.is_null_at(i));
+            ASSERT_FALSE(key_col.is_null_at(i));
+            StringRef k = key_col.get_nested_column().get_data_at(i);
+            EXPECT_EQ("k0", std::string(k.data, k.size));
+        }
+        fn.process_close();
+    }
+
+    // Case 2: SQL NULL input — current_empty() is true → insert_many_defaults.
+    {
+        auto block = build_jsonb_input_block({{""}}); // empty string → SQL 
NULL
+        ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+        fn.process_row(0);
+        ASSERT_TRUE(fn.current_empty());
+
+        auto out_col = struct_dt->create_column();
+        fn.get_same_many_values(out_col, 2);
+
+        ASSERT_EQ(2u, out_col->size());
+        const auto& nullable_out = assert_cast<const 
ColumnNullable&>(*out_col);
+        EXPECT_TRUE(nullable_out.is_null_at(0));
+        EXPECT_TRUE(nullable_out.is_null_at(1));
+        fn.process_close();
+    }
+}
+
+TEST_F(TableFunctionTest, vjson_each_outer) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    // set_outer() correctly sets the is_outer flag
+    EXPECT_FALSE(fn.is_outer());
+    fn.set_outer();
+    EXPECT_TRUE(fn.is_outer());
+
+    // Normal object: outer flag does not affect KV expansion
+    {
+        auto block = build_jsonb_input_block({{R"({"a":"foo","b":123})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), true);
+        ASSERT_EQ(2u, rows.size());
+        EXPECT_EQ("a", rows[0].first);
+        EXPECT_EQ("\"foo\"", rows[0].second);
+        EXPECT_EQ("b", rows[1].first);
+        EXPECT_EQ("123", rows[1].second);
+    }
+
+    // For NULL / empty-object / non-object inputs: current_empty() is true.
+    // The operator calls get_value() unconditionally when is_outer() — verify 
that
+    // get_value() inserts exactly one default (NULL) struct row in each case.
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    for (const char* input : {"", "{}", "[1,2,3]"}) {
+        auto block = build_jsonb_input_block({{input}});
+        ASSERT_TRUE(fn.process_init(block.get(), &rs).ok()) << "input: " << 
input;
+        fn.process_row(0);
+        EXPECT_TRUE(fn.current_empty()) << "input: " << input;
+
+        auto out_col = struct_dt->create_column();
+        fn.get_value(out_col, 1);
+        ASSERT_EQ(1u, out_col->size()) << "input: " << input;
+        EXPECT_TRUE(out_col->is_null_at(0)) << "input: " << input;
+        fn.process_close();
+    }
+}
+
+TEST_F(TableFunctionTest, vjson_each_text_outer) {
+    init_expr_context(1);
+    VJsonEachTextTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    EXPECT_FALSE(fn.is_outer());
+    fn.set_outer();
+    EXPECT_TRUE(fn.is_outer());
+
+    // Normal object: text mode (strings unquoted), outer flag does not affect 
expansion
+    {
+        auto block = build_jsonb_input_block({{R"({"a":"foo","b":123})"}});
+        auto rows = run_json_each_fn(&fn, block.get(), false);
+        ASSERT_EQ(2u, rows.size());
+        EXPECT_EQ("a", rows[0].first);
+        EXPECT_EQ("foo", rows[0].second);
+        EXPECT_EQ("b", rows[1].first);
+        EXPECT_EQ("123", rows[1].second);
+    }
+
+    // NULL / empty-object / non-object → current_empty(), get_value() inserts 
one default row
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    for (const char* input : {"", "{}", "[1,2,3]"}) {
+        auto block = build_jsonb_input_block({{input}});
+        ASSERT_TRUE(fn.process_init(block.get(), &rs).ok()) << "input: " << 
input;
+        fn.process_row(0);
+        EXPECT_TRUE(fn.current_empty()) << "input: " << input;
+
+        auto out_col = struct_dt->create_column();
+        fn.get_value(out_col, 1);
+        ASSERT_EQ(1u, out_col->size()) << "input: " << input;
+        EXPECT_TRUE(out_col->is_null_at(0)) << "input: " << input;
+        fn.process_close();
+    }
+}
+
+// Helper: build a one-column JSONB block with raw bytes, bypassing JSON parse.
+static std::unique_ptr<Block> build_raw_jsonb_block(
+        const std::vector<std::pair<std::string, bool>>& entries) {
+    auto str_col = ColumnString::create();
+    auto null_col = ColumnUInt8::create();
+    for (const auto& [data, is_null] : entries) {
+        if (is_null) {
+            str_col->insert_default();
+            null_col->insert_value(1);
+        } else {
+            str_col->insert_data(data.data(), data.size());
+            null_col->insert_value(0);
+        }
+    }
+    auto col = ColumnNullable::create(std::move(str_col), std::move(null_col));
+    auto block = Block::create_unique();
+    block->insert({std::move(col),
+                   make_nullable(DataTypeFactory::instance().create_data_type(
+                           doris::PrimitiveType::TYPE_JSONB, false)),
+                   "jval"});
+    return block;
+}
+
+// Corrupt JSONB binary input — hits checkAndCreateDocument failure branch in 
process_row.
+TEST_F(TableFunctionTest, vjson_each_corrupt_jsonb_binary) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    // Garbage bytes marked as non-null — checkAndCreateDocument should fail,
+    // process_row should leave _cur_size=0 (current_empty() == true).
+    std::string garbage = "\xDE\xAD\xBE\xEF";
+    auto block = build_raw_jsonb_block({{garbage, false}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+
+    // Directly verify internal state via BE_TEST-exposed members
+    EXPECT_TRUE(fn.current_empty());
+    EXPECT_TRUE(!fn.test_kv_pairs_first());
+    EXPECT_TRUE(!fn.test_kv_pairs_second());
+
+    fn.process_close();
+}
+
+// Corrupt JSONB + outer mode → get_value should insert exactly one default 
(NULL) row.
+TEST_F(TableFunctionTest, vjson_each_corrupt_jsonb_outer) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_outer();
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    std::string garbage = "\x00\x01\x02\x03";
+    auto block = build_raw_jsonb_block({{garbage, false}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    EXPECT_TRUE(fn.current_empty());
+
+    auto out_col = struct_dt->create_column();
+    fn.get_value(out_col, 1);
+    ASSERT_EQ(1U, out_col->size());
+    EXPECT_TRUE(out_col->is_null_at(0)); // outer: one default NULL row
+
+    fn.process_close();
+}
+
+// Corrupt JSONB in json_each_text mode — same behaviour.
+TEST_F(TableFunctionTest, vjson_each_text_corrupt_jsonb_binary) {
+    init_expr_context(1);
+    VJsonEachTextTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    std::string garbage = "\xFF\xFE\xFD";
+    auto block = build_raw_jsonb_block({{garbage, false}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+
+    EXPECT_TRUE(fn.current_empty());
+    EXPECT_TRUE(!fn.test_kv_pairs_first());
+    EXPECT_TRUE(!fn.test_kv_pairs_second());
+
+    fn.process_close();
+}
+
+// Corrupt JSONB in json_each_text_outer mode should still emit one default 
row.
+TEST_F(TableFunctionTest, vjson_each_text_corrupt_jsonb_outer) {
+    init_expr_context(1);
+    VJsonEachTextTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_outer();
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    std::string garbage = "\xFA\xFB\xFC";
+    auto block = build_raw_jsonb_block({{garbage, false}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    EXPECT_TRUE(fn.current_empty());
+
+    auto out_col = struct_dt->create_column();
+    fn.get_value(out_col, 1);
+    ASSERT_EQ(1U, out_col->size());
+    EXPECT_TRUE(out_col->is_null_at(0));
+
+    fn.process_close();
+}
+
+// get_same_many_values at non-zero offset — verify it copies current offset 
entry,
+// not the first entry.
+TEST_F(TableFunctionTest, vjson_each_get_same_many_values_nonzero_offset) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    // Object with 3 keys: k0, k1, k2
+    auto block = 
build_jsonb_input_block({{R"({"k0":"v0","k1":"v1","k2":"v2"})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    // Consume the first entry via get_value(max_step=1) to advance 
_cur_offset to 1
+    {
+        auto tmp_col = struct_dt->create_column();
+        int step = fn.get_value(tmp_col, 1);
+        EXPECT_EQ(1, step);
+        EXPECT_FALSE(fn.eos());
+    }
+
+    // Now _cur_offset == 1. get_same_many_values should replicate entry at 
offset 1 ("k1").
+    auto out_col = struct_dt->create_column();
+    fn.get_same_many_values(out_col, 3);
+
+    const auto& nullable_out = assert_cast<const ColumnNullable&>(*out_col);
+    ASSERT_EQ(3U, nullable_out.size());
+    const auto& struct_col = assert_cast<const 
ColumnStruct&>(nullable_out.get_nested_column());
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(0));
+    for (size_t i = 0; i < 3; ++i) {
+        EXPECT_FALSE(nullable_out.is_null_at(i));
+        ASSERT_FALSE(key_col.is_null_at(i));
+        StringRef k = key_col.get_nested_column().get_data_at(i);
+        EXPECT_EQ("k1", std::string(k.data, k.size))
+                << "Expected entry at offset 1, got '" << std::string(k.data, 
k.size) << "'";
+    }
+    fn.process_close();
+}
+
+// Same test for json_each_text mode.
+TEST_F(TableFunctionTest, vjson_each_text_get_same_many_values_nonzero_offset) 
{
+    init_expr_context(1);
+    VJsonEachTextTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"a":"A","b":"B","c":"C"})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    // Advance offset past first entry
+    {
+        auto tmp_col = struct_dt->create_column();
+        fn.get_value(tmp_col, 1);
+    }
+
+    auto out_col = struct_dt->create_column();
+    fn.get_same_many_values(out_col, 2);
+
+    const auto& nullable_out = assert_cast<const ColumnNullable&>(*out_col);
+    ASSERT_EQ(2U, nullable_out.size());
+    const auto& struct_col = assert_cast<const 
ColumnStruct&>(nullable_out.get_nested_column());
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(0));
+    const auto& val_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(1));
+    for (size_t i = 0; i < 2; ++i) {
+        StringRef k = key_col.get_nested_column().get_data_at(i);
+        EXPECT_EQ("b", std::string(k.data, k.size)); // offset 1 = "b"
+        StringRef v = val_col.get_nested_column().get_data_at(i);
+        EXPECT_EQ("B", std::string(v.data, v.size)); // text mode: unquoted
+    }
+    fn.process_close();
+}
+
+// process_close — directly verify private members are reset.
+TEST_F(TableFunctionTest, vjson_each_process_close_internal_state) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"a":1,"b":2})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+
+    // Before close: members should be populated
+    EXPECT_TRUE(fn.test_json_column());
+    EXPECT_TRUE(fn.test_kv_pairs_first());
+    EXPECT_TRUE(fn.test_kv_pairs_second());
+    EXPECT_FALSE(fn.current_empty());
+
+    fn.process_close();
+
+    // After close: all pointers null, _cur_size reset
+    EXPECT_TRUE(!fn.test_json_column());
+    EXPECT_TRUE(!fn.test_kv_pairs_first());
+    EXPECT_TRUE(!fn.test_kv_pairs_second());
+    EXPECT_TRUE(fn.current_empty());
+}
+
+// process_row with _is_const — second call should skip re-parsing when 
_cur_size > 0.
+// Verify by inspecting _kv_pairs: they should remain from the first call.
+TEST_F(TableFunctionTest, vjson_each_process_row_const_column) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    // Build a const column (ColumnConst wrapping a single JSONB value)
+    JsonbWriter writer;
+    const std::string json_const_obj = R"({"x":10,"y":20})";
+    ASSERT_TRUE(JsonbParser::parse(json_const_obj.data(), 
json_const_obj.size(), writer).ok());
+    auto inner_str_col = ColumnString::create();
+    auto inner_null_col = ColumnUInt8::create();
+    inner_str_col->insert_data(writer.getOutput()->getBuffer(), 
writer.getOutput()->getSize());
+    inner_null_col->insert_value(0);
+    auto inner_nullable =
+            ColumnNullable::create(std::move(inner_str_col), 
std::move(inner_null_col));
+    // Wrap as ColumnConst with 3 logical rows
+    auto const_col = ColumnConst::create(std::move(inner_nullable), 3);
+
+    auto block = Block::create_unique();
+    block->insert({std::move(const_col),
+                   make_nullable(DataTypeFactory::instance().create_data_type(
+                           doris::PrimitiveType::TYPE_JSONB, false)),
+                   "jval"});
+
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+
+    // First process_row: parses and populates _kv_pairs
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+    auto* kv_first_ptr = fn.test_kv_pairs_first().get();
+    ASSERT_NE(nullptr, kv_first_ptr);
+
+    // Reset offset to simulate next iteration (the operator resets between 
rows)
+    fn.reset();
+
+    // Second process_row on a different logical row: should skip reparsing 
(_is_const && _cur_size>0)
+    fn.process_row(1);
+    EXPECT_FALSE(fn.current_empty());
+    // _kv_pairs.first pointer should be identical — no re-allocation
+    EXPECT_EQ(kv_first_ptr, fn.test_kv_pairs_first().get());
+
+    fn.process_close();
+}
+
+TEST_F(TableFunctionTest, vjson_each_process_row_const_empty_object_column) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    JsonbWriter writer;
+    const std::string json_empty_obj = R"({})";
+    ASSERT_TRUE(JsonbParser::parse(json_empty_obj.data(), 
json_empty_obj.size(), writer).ok());
+    auto inner_str_col = ColumnString::create();
+    auto inner_null_col = ColumnUInt8::create();
+    inner_str_col->insert_data(writer.getOutput()->getBuffer(), 
writer.getOutput()->getSize());
+    inner_null_col->insert_value(0);
+    auto inner_nullable =
+            ColumnNullable::create(std::move(inner_str_col), 
std::move(inner_null_col));
+    auto const_col = ColumnConst::create(std::move(inner_nullable), 2);
+
+    auto block = Block::create_unique();
+    block->insert({std::move(const_col),
+                   make_nullable(DataTypeFactory::instance().create_data_type(
+                           doris::PrimitiveType::TYPE_JSONB, false)),
+                   "jval"});
+
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+
+    fn.process_row(0);
+    EXPECT_TRUE(fn.current_empty());
+    EXPECT_TRUE(!fn.test_kv_pairs_first());
+    EXPECT_TRUE(!fn.test_kv_pairs_second());
+
+    fn.reset();
+    fn.process_row(1);
+    EXPECT_TRUE(fn.current_empty());
+    EXPECT_TRUE(!fn.test_kv_pairs_first());
+    EXPECT_TRUE(!fn.test_kv_pairs_second());
+
+    fn.process_close();
+}
+
+// get_value on current_empty — inserts exactly one default and returns 1.
+TEST_F(TableFunctionTest, vjson_each_get_value_current_empty) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    // Empty object → current_empty
+    auto block = build_jsonb_input_block({{"{}"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    EXPECT_TRUE(fn.current_empty());
+
+    auto out_col = struct_dt->create_column();
+    int step = fn.get_value(out_col, 5); // max_step ignored when empty
+    EXPECT_EQ(1, step);
+    ASSERT_EQ(1U, out_col->size());
+    EXPECT_TRUE(out_col->is_null_at(0)); // default row is NULL struct
+    EXPECT_TRUE(fn.eos());
+
+    fn.process_close();
+}
+
+// get_value with max_step > _cur_size — clamped to actual size.
+TEST_F(TableFunctionTest, vjson_each_get_value_max_step_clamped) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"a":1,"b":2})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    auto out_col = struct_dt->create_column();
+    int step = fn.get_value(out_col, 100); // request 100, only 2 available
+    EXPECT_EQ(2, step);
+    ASSERT_EQ(2U, out_col->size());
+    EXPECT_TRUE(fn.eos());
+
+    fn.process_close();
+}
+
+TEST_F(TableFunctionTest, vjson_each_get_value_zero_max_step) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    fn.set_nullable();
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt =
+            make_nullable(std::make_shared<DataTypeStruct>(DataTypes {key_dt, 
val_dt}));
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"a":1,"b":2})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    auto out_col = struct_dt->create_column();
+    int step = fn.get_value(out_col, 0);
+    EXPECT_EQ(0, step);
+    EXPECT_EQ(0U, out_col->size());
+    EXPECT_FALSE(fn.eos());
+
+    fn.process_close();
+}
+
+// Verify _kv_pairs content directly after process_row (json_each mode) —
+// value column should contain JSONB binary, not JSON text.
+TEST_F(TableFunctionTest, vjson_each_kv_pairs_jsonb_binary) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"k":"hello"})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    // Inspect key column
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(*fn.test_kv_pairs_first());
+    ASSERT_EQ(1U, key_col.size());
+    ASSERT_FALSE(key_col.is_null_at(0));
+    StringRef key = key_col.get_nested_column().get_data_at(0);
+    EXPECT_EQ("k", std::string(key.data, key.size));
+
+    // Inspect value column — should be valid JSONB binary, not plain text
+    const auto& val_col = assert_cast<const 
ColumnNullable&>(*fn.test_kv_pairs_second());
+    ASSERT_EQ(1U, val_col.size());
+    ASSERT_FALSE(val_col.is_null_at(0));
+    StringRef val_raw = val_col.get_nested_column().get_data_at(0);
+    // Verify it's valid JSONB by parsing it back
+    const JsonbDocument* doc = nullptr;
+    ASSERT_TRUE(JsonbDocument::checkAndCreateDocument(val_raw.data, 
val_raw.size, &doc).ok());
+    ASSERT_NE(nullptr, doc);
+    ASSERT_NE(nullptr, doc->getValue());
+    EXPECT_TRUE(doc->getValue()->isString());
+
+    fn.process_close();
+}
+
+// Verify _kv_pairs content directly after process_row (json_each_text mode) —
+// string values should be raw blob content (unquoted), not JSONB binary.
+TEST_F(TableFunctionTest, vjson_each_text_kv_pairs_plain_text) {
+    init_expr_context(1);
+    VJsonEachTextTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"k":"hello","n":42})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    const auto& val_col = assert_cast<const 
ColumnNullable&>(*fn.test_kv_pairs_second());
+    ASSERT_EQ(2U, val_col.size());
+
+    // Find the entries (order depends on JSONB iteration)
+    std::map<std::string, std::string> kv;
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(*fn.test_kv_pairs_first());
+    for (size_t i = 0; i < 2; ++i) {
+        StringRef kr = key_col.get_nested_column().get_data_at(i);
+        StringRef vr = val_col.get_nested_column().get_data_at(i);
+        kv[std::string(kr.data, kr.size)] = std::string(vr.data, vr.size);
+    }
+    // Text mode: string "hello" unquoted, number "42" as plain text
+    EXPECT_EQ("hello", kv["k"]);
+    EXPECT_EQ("42", kv["n"]);
+
+    fn.process_close();
+}
+
+// Verify _kv_pairs for JSON null value — should produce SQL NULL (is_null_at 
== true).
+TEST_F(TableFunctionTest, vjson_each_kv_pairs_null_value) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"k":null})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    const auto& val_col = assert_cast<const 
ColumnNullable&>(*fn.test_kv_pairs_second());
+    ASSERT_EQ(1U, val_col.size());
+    EXPECT_TRUE(val_col.is_null_at(0)); // JSON null → SQL NULL via 
insert_default
+
+    fn.process_close();
+}
+
+// forward() and eos() interaction — test the base class forward logic
+// through the json_each function.
+TEST_F(TableFunctionTest, vjson_each_forward_eos) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"a":1,"b":2})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+    ASSERT_FALSE(fn.current_empty());
+
+    EXPECT_FALSE(fn.eos());
+    fn.forward(1); // offset 0 → 1
+    EXPECT_FALSE(fn.eos());
+    fn.forward(1); // offset 1 → 2, == _cur_size → eos
+    EXPECT_TRUE(fn.eos());
+
+    fn.process_close();
+}
+
+// Non-nullable get_value path (without set_nullable) — struct_col directly, 
no ColumnNullable wrapper.
+TEST_F(TableFunctionTest, vjson_each_get_value_non_nullable) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    // Intentionally NOT calling fn.set_nullable()
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    // Non-nullable struct type — no wrapping Nullable
+    DataTypePtr struct_dt = std::make_shared<DataTypeStruct>(DataTypes 
{key_dt, val_dt});
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"a":1})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+
+    auto out_col = struct_dt->create_column();
+    int step = fn.get_value(out_col, 10);
+    EXPECT_EQ(1, step);
+    ASSERT_EQ(1U, out_col->size());
+
+    // Directly a ColumnStruct, not wrapped in ColumnNullable
+    const auto& struct_col = assert_cast<const ColumnStruct&>(*out_col);
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(0));
+    StringRef k = key_col.get_nested_column().get_data_at(0);
+    EXPECT_EQ("a", std::string(k.data, k.size));
+
+    fn.process_close();
+}
+
+// Non-nullable get_same_many_values path.
+TEST_F(TableFunctionTest, vjson_each_get_same_many_values_non_nullable) {
+    init_expr_context(1);
+    VJsonEachTableFn fn;
+    fn.set_expr_context(_ctx);
+    // NOT calling fn.set_nullable()
+
+    DataTypePtr key_dt = 
make_nullable(DataTypeFactory::instance().create_data_type(
+            doris::PrimitiveType::TYPE_VARCHAR, false));
+    DataTypePtr val_dt = make_nullable(
+            
DataTypeFactory::instance().create_data_type(doris::PrimitiveType::TYPE_JSONB, 
false));
+    DataTypePtr struct_dt = std::make_shared<DataTypeStruct>(DataTypes 
{key_dt, val_dt});
+
+    TQueryOptions q_opts;
+    TQueryGlobals q_globals;
+    RuntimeState rs(q_opts, q_globals);
+
+    auto block = build_jsonb_input_block({{R"({"x":1})"}});
+    ASSERT_TRUE(fn.process_init(block.get(), &rs).ok());
+    fn.process_row(0);
+
+    auto out_col = struct_dt->create_column();
+    fn.get_same_many_values(out_col, 2);
+    ASSERT_EQ(2U, out_col->size());
+
+    const auto& struct_col = assert_cast<const ColumnStruct&>(*out_col);
+    const auto& key_col = assert_cast<const 
ColumnNullable&>(struct_col.get_column(0));
+    for (size_t i = 0; i < 2; ++i) {
+        StringRef k = key_col.get_nested_column().get_data_at(i);
+        EXPECT_EQ("x", std::string(k.data, k.size));
+    }
+
+    fn.process_close();
+}
+
 } // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableGeneratingFunctions.java
 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableGeneratingFunctions.java
index 1ad8b5ccf4b..f764da5718c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableGeneratingFunctions.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinTableGeneratingFunctions.java
@@ -38,6 +38,10 @@ import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeOut
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeSplit;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeSplitOuter;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeVariantArray;
+import org.apache.doris.nereids.trees.expressions.functions.generator.JsonEach;
+import 
org.apache.doris.nereids.trees.expressions.functions.generator.JsonEachOuter;
+import 
org.apache.doris.nereids.trees.expressions.functions.generator.JsonEachText;
+import 
org.apache.doris.nereids.trees.expressions.functions.generator.JsonEachTextOuter;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.PosExplode;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.PosExplodeOuter;
 import org.apache.doris.nereids.trees.expressions.functions.generator.Unnest;
@@ -78,6 +82,10 @@ public class BuiltinTableGeneratingFunctions implements 
FunctionHelper {
             tableGenerating(ExplodeJsonArrayJson.class, 
"explode_json_array_json"),
             tableGenerating(ExplodeJsonArrayJsonOuter.class, 
"explode_json_array_json_outer"),
             tableGenerating(ExplodeVariantArray.class, 
"explode_variant_array"),
+            tableGenerating(JsonEach.class, "json_each"),
+            tableGenerating(JsonEachOuter.class, "json_each_outer"),
+            tableGenerating(JsonEachText.class, "json_each_text"),
+            tableGenerating(JsonEachTextOuter.class, "json_each_text_outer"),
             tableGenerating(PosExplode.class, "posexplode"),
             tableGenerating(PosExplodeOuter.class, "posexplode_outer"),
             tableGenerating(Unnest.class, "unnest")
@@ -89,6 +97,8 @@ public class BuiltinTableGeneratingFunctions implements 
FunctionHelper {
             
.add("explode_json_array_string").add("explode_json_array_json").add("explode_json_array_int_outer")
             
.add("explode_json_array_double_outer").add("explode_json_array_string_outer")
             
.add("explode_json_array_json_outer").add("explode_split").add("explode_split_outer")
+            .add("json_each").add("json_each_outer")
+            .add("json_each_text").add("json_each_text_outer")
             .add("posexplode").add("posexplode_outer").build();
 
     public Set<String> getReturnManyColumnFunctions() {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEach.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEach.java
new file mode 100644
index 00000000000..acb7a1a0891
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEach.java
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.generator;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable;
+import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.JsonType;
+import org.apache.doris.nereids.types.StringType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * json_each(json) expands the top-level JSON object into a set of key/value
+ * pairs.
+ * Returns: Struct(key VARCHAR, value JSON) — one row per top-level key.
+ *
+ * Example:
+ * SELECT key, value FROM LATERAL VIEW json_each('{"a":"foo","b":"bar"}') t AS
+ * key, value
+ * → key="a", value="foo" (JSON-formatted)
+ * → key="b", value="bar"
+ */
+public class JsonEach extends TableGeneratingFunction implements 
UnaryExpression, AlwaysNullable {
+
+    public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+            FunctionSignature.ret(StructLiteral.constructStructType(
+                    ImmutableList.of(StringType.INSTANCE, JsonType.INSTANCE)))
+                    .args(JsonType.INSTANCE));
+
+    /**
+     * Constructor with 1 argument.
+     */
+    public JsonEach(Expression arg) {
+        super("json_each", arg);
+    }
+
+    /** Constructor for withChildren and reuse signature. */
+    private JsonEach(GeneratorFunctionParams functionParams) {
+        super(functionParams);
+    }
+
+    @Override
+    public JsonEach withChildren(List<Expression> children) {
+        Preconditions.checkArgument(children.size() == 1);
+        return new JsonEach(getFunctionParams(children));
+    }
+
+    @Override
+    public List<FunctionSignature> getSignatures() {
+        return SIGNATURES;
+    }
+
+    @Override
+    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+        return visitor.visitJsonEach(this, context);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachOuter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachOuter.java
new file mode 100644
index 00000000000..1c9b2e298d0
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachOuter.java
@@ -0,0 +1,72 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.generator;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable;
+import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.JsonType;
+import org.apache.doris.nereids.types.StringType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * json_each_outer(json) is json_each with outer semantics: emits one NULL row
+ * when the input is NULL or not a JSON object, instead of producing no rows.
+ */
+public class JsonEachOuter extends TableGeneratingFunction implements 
UnaryExpression, AlwaysNullable {
+
+    public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+            FunctionSignature.ret(StructLiteral.constructStructType(
+                    ImmutableList.of(StringType.INSTANCE, JsonType.INSTANCE)))
+                    .args(JsonType.INSTANCE));
+
+    /**
+     * Constructor with 1 argument.
+     */
+    public JsonEachOuter(Expression arg) {
+        super("json_each_outer", arg);
+    }
+
+    /** Constructor for withChildren and reuse signature. */
+    private JsonEachOuter(GeneratorFunctionParams functionParams) {
+        super(functionParams);
+    }
+
+    @Override
+    public JsonEachOuter withChildren(List<Expression> children) {
+        Preconditions.checkArgument(children.size() == 1);
+        return new JsonEachOuter(getFunctionParams(children));
+    }
+
+    @Override
+    public List<FunctionSignature> getSignatures() {
+        return SIGNATURES;
+    }
+
+    @Override
+    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+        return visitor.visitJsonEachOuter(this, context);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachText.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachText.java
new file mode 100644
index 00000000000..21faaf47de3
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachText.java
@@ -0,0 +1,80 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.generator;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable;
+import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.JsonType;
+import org.apache.doris.nereids.types.StringType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * json_each_text(json) expands the top-level JSON object into a set of
+ * key/value pairs.
+ * Returns: Struct(key VARCHAR, value VARCHAR) — the JSON value is returned as
+ * plain text.
+ *
+ * Example:
+ * SELECT key, value FROM LATERAL VIEW json_each_text('{"a":"foo","b":"bar"}') 
t
+ * AS key, value
+ * → key="a", value=foo (plain string, not JSON-quoted)
+ * → key="b", value=bar
+ */
+public class JsonEachText extends TableGeneratingFunction implements 
UnaryExpression, AlwaysNullable {
+
+    public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+            FunctionSignature.ret(StructLiteral.constructStructType(
+                    ImmutableList.of(StringType.INSTANCE, 
StringType.INSTANCE)))
+                    .args(JsonType.INSTANCE));
+
+    /**
+     * Constructor with 1 argument.
+     */
+    public JsonEachText(Expression arg) {
+        super("json_each_text", arg);
+    }
+
+    /** Constructor for withChildren and reuse signature. */
+    private JsonEachText(GeneratorFunctionParams functionParams) {
+        super(functionParams);
+    }
+
+    @Override
+    public JsonEachText withChildren(List<Expression> children) {
+        Preconditions.checkArgument(children.size() == 1);
+        return new JsonEachText(getFunctionParams(children));
+    }
+
+    @Override
+    public List<FunctionSignature> getSignatures() {
+        return SIGNATURES;
+    }
+
+    @Override
+    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+        return visitor.visitJsonEachText(this, context);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachTextOuter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachTextOuter.java
new file mode 100644
index 00000000000..8cc33cf529e
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/generator/JsonEachTextOuter.java
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.generator;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable;
+import org.apache.doris.nereids.trees.expressions.literal.StructLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.JsonType;
+import org.apache.doris.nereids.types.StringType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * json_each_text_outer(json) is json_each_text with outer semantics: emits one
+ * NULL row when the input is NULL or not a JSON object, instead of producing 
no
+ * rows.
+ */
+public class JsonEachTextOuter extends TableGeneratingFunction implements 
UnaryExpression, AlwaysNullable {
+
+    public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+            FunctionSignature.ret(StructLiteral.constructStructType(
+                    ImmutableList.of(StringType.INSTANCE, 
StringType.INSTANCE)))
+                    .args(JsonType.INSTANCE));
+
+    /**
+     * Constructor with 1 argument.
+     */
+    public JsonEachTextOuter(Expression arg) {
+        super("json_each_text_outer", arg);
+    }
+
+    /** Constructor for withChildren and reuse signature. */
+    private JsonEachTextOuter(GeneratorFunctionParams functionParams) {
+        super(functionParams);
+    }
+
+    @Override
+    public JsonEachTextOuter withChildren(List<Expression> children) {
+        Preconditions.checkArgument(children.size() == 1);
+        return new JsonEachTextOuter(getFunctionParams(children));
+    }
+
+    @Override
+    public List<FunctionSignature> getSignatures() {
+        return SIGNATURES;
+    }
+
+    @Override
+    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+        return visitor.visitJsonEachTextOuter(this, context);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableGeneratingFunctionVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableGeneratingFunctionVisitor.java
index 85af01ebd61..384b3209ee5 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableGeneratingFunctionVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/TableGeneratingFunctionVisitor.java
@@ -38,6 +38,10 @@ import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeOut
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeSplit;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeSplitOuter;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.ExplodeVariantArray;
+import org.apache.doris.nereids.trees.expressions.functions.generator.JsonEach;
+import 
org.apache.doris.nereids.trees.expressions.functions.generator.JsonEachOuter;
+import 
org.apache.doris.nereids.trees.expressions.functions.generator.JsonEachText;
+import 
org.apache.doris.nereids.trees.expressions.functions.generator.JsonEachTextOuter;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.PosExplode;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.PosExplodeOuter;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.TableGeneratingFunction;
@@ -79,6 +83,22 @@ public interface TableGeneratingFunctionVisitor<R, C> {
         return visitTableGeneratingFunction(explodeOuter, context);
     }
 
+    default R visitJsonEach(JsonEach jsonEach, C context) {
+        return visitTableGeneratingFunction(jsonEach, context);
+    }
+
+    default R visitJsonEachOuter(JsonEachOuter jsonEachOuter, C context) {
+        return visitTableGeneratingFunction(jsonEachOuter, context);
+    }
+
+    default R visitJsonEachText(JsonEachText jsonEachText, C context) {
+        return visitTableGeneratingFunction(jsonEachText, context);
+    }
+
+    default R visitJsonEachTextOuter(JsonEachTextOuter jsonEachTextOuter, C 
context) {
+        return visitTableGeneratingFunction(jsonEachTextOuter, context);
+    }
+
     default R visitExplodeNumbers(ExplodeNumbers explodeNumbers, C context) {
         return visitTableGeneratingFunction(explodeNumbers, context);
     }
diff --git 
a/regression-test/data/query_p0/sql_functions/table_function/json_each.out 
b/regression-test/data/query_p0/sql_functions/table_function/json_each.out
new file mode 100644
index 00000000000..fbf7a7d36aa
--- /dev/null
+++ b/regression-test/data/query_p0/sql_functions/table_function/json_each.out
@@ -0,0 +1,174 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !json_each_basic --
+1      a       "foo"
+1      b       "bar"
+
+-- !json_each_mixed --
+2      x       1
+2      y       true
+2      z       \N
+
+-- !json_each_empty --
+
+-- !json_each_null_input --
+
+-- !json_each_all --
+1      a       "foo"
+1      b       "bar"
+2      x       1
+2      y       true
+2      z       \N
+
+-- !json_each_literal --
+name   "doris"
+version        3
+
+-- !json_each_neg_false --
+5      bool_f  false
+5      neg     -1
+
+-- !json_each_unicode --
+6      cn      "中文"
+
+-- !json_each_non_object_str --
+
+-- !json_each_non_object_arr --
+
+-- !json_each_complex --
+9      arr     [1,2]
+9      sub     {"x":1}
+
+-- !json_each_text_basic --
+1      a       foo
+1      b       bar
+
+-- !json_each_text_mixed --
+2      x       1
+2      y       true
+2      z       \N
+
+-- !json_each_text_empty --
+
+-- !json_each_text_null_input --
+
+-- !json_each_text_all --
+1      a       foo
+1      b       bar
+2      x       1
+2      y       true
+2      z       \N
+
+-- !json_each_text_literal --
+name   doris
+version        3
+
+-- !json_each_text_neg_false --
+5      bool_f  false
+5      neg     -1
+
+-- !json_each_text_unicode --
+6      cn      中文
+
+-- !json_each_text_non_object_str --
+
+-- !json_each_text_non_object_arr --
+
+-- !json_each_text_complex --
+9      arr     [1,2]
+9      sub     {"x":1}
+
+-- !json_each_outer_null_input --
+4      \N      \N
+
+-- !json_each_outer_empty --
+3      \N      \N
+
+-- !json_each_outer_basic --
+1      a       "foo"
+1      b       "bar"
+
+-- !json_each_outer_non_object --
+7      \N      \N
+8      \N      \N
+
+-- !json_each_outer_all --
+1      a       "foo"
+1      b       "bar"
+2      x       1
+2      y       true
+2      z       \N
+3      \N      \N
+4      \N      \N
+
+-- !json_each_text_outer_null_input --
+4      \N      \N
+
+-- !json_each_text_outer_empty --
+3      \N      \N
+
+-- !json_each_text_outer_basic --
+1      a       foo
+1      b       bar
+
+-- !json_each_text_outer_non_object --
+7      \N      \N
+8      \N      \N
+
+-- !json_each_text_outer_all --
+1      a       foo
+1      b       bar
+2      x       1
+2      y       true
+2      z       \N
+3      \N      \N
+4      \N      \N
+
+-- !multi_lateral_nested --
+9      sub     x       1
+
+-- !multi_lateral_const --
+1      a       x       1
+1      a       y       2
+1      b       x       1
+1      b       y       2
+
+-- !multi_lateral_three_mixed --
+1      a       a       x
+1      a       a       y
+1      a       a       z
+1      a       b       x
+1      a       b       y
+1      a       b       z
+1      b       a       x
+1      b       a       y
+1      b       a       z
+1      b       b       x
+1      b       b       y
+1      b       b       z
+
+-- !multi_lateral_cartesian --
+a      1       x       10
+a      1       y       20
+b      2       x       10
+b      2       y       20
+
+-- !corner_deep_nesting --
+level1 {"level2":{"level3":"deep"}}
+
+-- !corner_special_keys --
+key with spaces        v1
+key-with-dash  v3
+key.with.dots  v2
+
+-- !corner_many_keys --
+10
+
+-- !corner_empty_key --
+       empty_key_value
+normal value
+
+-- !corner_const_multi_block --
+const  value
+const  value
+const  value
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/table_function/json_each.groovy 
b/regression-test/suites/query_p0/sql_functions/table_function/json_each.groovy
new file mode 100644
index 00000000000..b80b4e49524
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/table_function/json_each.groovy
@@ -0,0 +1,419 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite('json_each') {
+    sql ''' DROP TABLE IF EXISTS jdata '''
+    sql '''
+        CREATE TABLE IF NOT EXISTS jdata (
+            id INT,
+            jval JSONB
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES("replication_num" = "1")
+    '''
+
+    sql """ INSERT INTO jdata VALUES
+        (1, '{"a":"foo","b":"bar"}'),
+        (2, '{"x":1,"y":true,"z":null}'),
+        (3, '{}'),
+        (4, NULL),
+        (5, '{"neg":-1,"bool_f":false}'),
+        (6, '{"cn":"\u4e2d\u6587"}'),
+        (7, '"a_string"'),
+        (8, '[1,2,3]'),
+        (9, '{"arr":[1,2],"sub":{"x":1}}')
+    """
+
+    // ---------- json_each ----------
+
+    // basic string values: value is JSONB, shown with JSON quotes
+    qt_json_each_basic '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 1
+        ORDER BY id, k
+    '''
+
+    // int / bool true / JSON null → SQL NULL
+    qt_json_each_mixed '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 2
+        ORDER BY id, k
+    '''
+
+    // empty object → 0 rows
+    qt_json_each_empty '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 3
+        ORDER BY id, k
+    '''
+
+    // SQL NULL input, non-outer → 0 rows
+    qt_json_each_null_input '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 4
+        ORDER BY id, k
+    '''
+
+    // ids 1-4 combined
+    qt_json_each_all '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id IN (1, 2, 3, 4)
+        ORDER BY id, k
+    '''
+
+    // inline literal
+    qt_json_each_literal """
+        SELECT k, v
+        FROM (SELECT 1) dummy
+        LATERAL VIEW json_each('{"name":"doris","version":3}') t AS k, v
+        ORDER BY k
+    """
+
+    // negative int, boolean false
+    qt_json_each_neg_false '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 5
+        ORDER BY id, k
+    '''
+
+    // unicode string value
+    qt_json_each_unicode '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 6
+        ORDER BY id, k
+    '''
+
+    // non-object input: JSON string → 0 rows
+    qt_json_each_non_object_str '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 7
+        ORDER BY id, k
+    '''
+
+    // non-object input: JSON array → 0 rows
+    qt_json_each_non_object_arr '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 8
+        ORDER BY id, k
+    '''
+
+    // complex value types (nested obj + array): values are JSONB
+    qt_json_each_complex '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each(jval) t AS k, v
+        WHERE id = 9
+        ORDER BY id, k
+    '''
+
+    // ---------- json_each_text ----------
+
+    // string values unquoted in text mode
+    qt_json_each_text_basic '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 1
+        ORDER BY id, k
+    '''
+
+    // int / bool / JSON null → SQL NULL
+    qt_json_each_text_mixed '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 2
+        ORDER BY id, k
+    '''
+
+    // empty object → 0 rows
+    qt_json_each_text_empty '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 3
+        ORDER BY id, k
+    '''
+
+    // SQL NULL input, non-outer → 0 rows
+    qt_json_each_text_null_input '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 4
+        ORDER BY id, k
+    '''
+
+    // ids 1-4 combined
+    qt_json_each_text_all '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id IN (1, 2, 3, 4)
+        ORDER BY id, k
+    '''
+
+    // inline literal: strings unquoted
+    qt_json_each_text_literal """
+        SELECT k, v
+        FROM (SELECT 1) dummy
+        LATERAL VIEW json_each_text('{"name":"doris","version":3}') t AS k, v
+        ORDER BY k
+    """
+
+    // negative int, boolean false
+    qt_json_each_text_neg_false '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 5
+        ORDER BY id, k
+    '''
+
+    // unicode string value: unquoted in text mode
+    qt_json_each_text_unicode '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 6
+        ORDER BY id, k
+    '''
+
+    // non-object input: JSON string → 0 rows
+    qt_json_each_text_non_object_str '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 7
+        ORDER BY id, k
+    '''
+
+    // non-object input: JSON array → 0 rows
+    qt_json_each_text_non_object_arr '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 8
+        ORDER BY id, k
+    '''
+
+    // complex value types in text mode: values are text representation
+    qt_json_each_text_complex '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text(jval) t AS k, v
+        WHERE id = 9
+        ORDER BY id, k
+    '''
+
+    // ---------- json_each_outer ----------
+
+    // outer: NULL input → 1 row with NULL k, NULL v (original row preserved)
+    qt_json_each_outer_null_input '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_outer(jval) t AS k, v
+        WHERE id = 4
+        ORDER BY id, k
+    '''
+
+    // outer: empty object → 1 row with NULL k, NULL v
+    qt_json_each_outer_empty '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_outer(jval) t AS k, v
+        WHERE id = 3
+        ORDER BY id, k
+    '''
+
+    // outer: normal object → same result as non-outer json_each
+    qt_json_each_outer_basic '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_outer(jval) t AS k, v
+        WHERE id = 1
+        ORDER BY id, k
+    '''
+
+    // outer: non-object inputs (string / array) → 1 row each with NULL k, 
NULL v
+    qt_json_each_outer_non_object '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_outer(jval) t AS k, v
+        WHERE id IN (7, 8)
+        ORDER BY id, k
+    '''
+
+    // outer: mixed ids 1-4: id=3 (empty) and id=4 (NULL) each emit one 
NULL-padded row
+    qt_json_each_outer_all '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_outer(jval) t AS k, v
+        WHERE id IN (1, 2, 3, 4)
+        ORDER BY id, k
+    '''
+
+    // ---------- json_each_text_outer ----------
+
+    // outer: NULL input → 1 row with NULL k, NULL v
+    qt_json_each_text_outer_null_input '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text_outer(jval) t AS k, v
+        WHERE id = 4
+        ORDER BY id, k
+    '''
+
+    // outer: empty object → 1 row with NULL k, NULL v
+    qt_json_each_text_outer_empty '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text_outer(jval) t AS k, v
+        WHERE id = 3
+        ORDER BY id, k
+    '''
+
+    // outer: normal object → same as json_each_text (strings unquoted)
+    qt_json_each_text_outer_basic '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text_outer(jval) t AS k, v
+        WHERE id = 1
+        ORDER BY id, k
+    '''
+
+    // outer: non-object inputs → 1 row each with NULL k, NULL v
+    qt_json_each_text_outer_non_object '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text_outer(jval) t AS k, v
+        WHERE id IN (7, 8)
+        ORDER BY id, k
+    '''
+
+    // outer: mixed ids 1-4
+    qt_json_each_text_outer_all '''
+        SELECT id, k, v
+        FROM jdata
+        LATERAL VIEW json_each_text_outer(jval) t AS k, v
+        WHERE id IN (1, 2, 3, 4)
+        ORDER BY id, k
+    '''
+
+    // ---------- Multiple LATERAL VIEW combinations ----------
+
+    // double json_each: expand nested object
+    qt_multi_lateral_nested '''
+        SELECT id, k1, k2, v2
+        FROM jdata
+        LATERAL VIEW json_each(jval) t1 AS k1, v1
+        LATERAL VIEW json_each(v1) t2 AS k2, v2
+        WHERE id = 9 AND k1 = 'sub'
+        ORDER BY k1, k2
+    '''
+
+    // json_each with const literal in second lateral view
+    qt_multi_lateral_const '''
+        SELECT id, k1, k2, v2
+        FROM jdata
+        LATERAL VIEW json_each(jval) t1 AS k1, v1
+        LATERAL VIEW json_each('{"x":1,"y":2}') t2 AS k2, v2
+        WHERE id = 1
+        ORDER BY k1, k2
+    '''
+
+    // three lateral views: column + two const literals with different sizes
+    qt_multi_lateral_three_mixed '''
+        SELECT id, k1, k2, k3
+        FROM jdata
+        LATERAL VIEW json_each(jval) t1 AS k1, v1
+        LATERAL VIEW json_each('{"a":1,"b":2}') t2 AS k2, v2
+        LATERAL VIEW json_each('{"x":1,"y":2,"z":3}') t3 AS k3, v3
+        WHERE id = 1
+        ORDER BY k1, k2, k3
+    '''
+
+    // multiple json_each on same row: cartesian product
+    qt_multi_lateral_cartesian '''
+        SELECT k1, v1, k2, v2
+        FROM (SELECT '{"a":1,"b":2}' AS j1, '{"x":10,"y":20}' AS j2) t
+        LATERAL VIEW json_each(j1) t1 AS k1, v1
+        LATERAL VIEW json_each(j2) t2 AS k2, v2
+        ORDER BY k1, k2
+    '''
+
+    // ---------- Corner cases ----------
+
+    // deeply nested object keys
+    qt_corner_deep_nesting '''
+        SELECT k, v
+        FROM (SELECT '{"level1":{"level2":{"level3":"deep"}}}' AS j) t
+        LATERAL VIEW json_each(j) t AS k, v
+        ORDER BY k
+    '''
+
+    // special characters in keys
+    qt_corner_special_keys '''
+        SELECT k, v
+        FROM (SELECT '{"key with 
spaces":"v1","key.with.dots":"v2","key-with-dash":"v3"}' AS j) t
+        LATERAL VIEW json_each_text(j) t AS k, v
+        ORDER BY k
+    '''
+
+    // large number of keys
+    qt_corner_many_keys '''
+        SELECT COUNT(*) AS key_count
+        FROM (SELECT 
'{"k1":1,"k2":2,"k3":3,"k4":4,"k5":5,"k6":6,"k7":7,"k8":8,"k9":9,"k10":10}' AS 
j) t
+        LATERAL VIEW json_each(j) t AS k, v
+    '''
+
+    // empty string key
+    qt_corner_empty_key '''
+        SELECT k, v
+        FROM (SELECT '{"":"empty_key_value","normal":"value"}' AS j) t
+        LATERAL VIEW json_each_text(j) t AS k, v
+        ORDER BY k
+    '''
+
+    // const input across multiple blocks: test process_close state reset
+    qt_corner_const_multi_block '''
+        SELECT k, v
+        FROM (
+            SELECT 1 AS id UNION ALL SELECT 2 UNION ALL SELECT 3
+        ) t
+        LATERAL VIEW json_each_text('{"const":"value"}') t AS k, v
+        ORDER BY id, k
+    '''
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to