Coverage Report

Created: 2026-11-11 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/duckdb/extension/json/json_functions/json_structure.cpp
Line
Count
Source
1
#include "duckdb/common/assert.hpp"
2
#include "duckdb/common/enum_util.hpp"
3
#include "duckdb/common/helper.hpp"
4
#include "duckdb/common/optional.hpp"
5
#include "duckdb/common/optional_ptr.hpp"
6
#include "duckdb/common/types.hpp"
7
#include "duckdb/common/unique_ptr.hpp"
8
#include "duckdb/common/vector.hpp"
9
#include "duckdb/function/function_set.hpp"
10
#include "duckdb/function/scalar_function.hpp"
11
#include "json_common.hpp"
12
#include "json_executors.hpp"
13
#include "json_functions.hpp"
14
#include "yyjson.hpp"
15
16
namespace duckdb {
17
18
namespace {
19
20
//===--------------------------------------------------------------------===//
21
// Structure nodes
22
//===--------------------------------------------------------------------===//
23
//! A structure node represents a subtree of the output schema.
24
struct StructureNode {
25
0
  explicit StructureNode(yyjson_mut_val &value_p) : value(value_p) {
26
0
  }
27
28
public:
29
  bool MergeType(LogicalTypeId type_p, yyjson_mut_doc *doc);
30
31
public:
32
  //! The inferred schema for the subtree associated with this structure node.
33
  reference<yyjson_mut_val> value;
34
  //! Merged input type; INVALID before discovery, ignored once incompatible.
35
  LogicalTypeId type = LogicalTypeId::INVALID;
36
  //! Conflicting types at this schema position forced the output to "JSON".
37
  bool incompatible = false;
38
39
  //! Shared schema node for all array elements.
40
  optional_ptr<StructureNode> array_child;
41
  //! Schema nodes for object fields, indexed by key.
42
  json_key_map_t<reference<StructureNode>> key_map;
43
44
private:
45
  void SetType(LogicalTypeId type_p);
46
  void MarkIncompatible(yyjson_mut_doc *doc);
47
};
48
49
0
bool StructureNode::MergeType(LogicalTypeId type_p, yyjson_mut_doc *doc) {
50
0
  D_ASSERT(type_p != LogicalTypeId::INVALID);
51
0
  if (incompatible) {
52
0
    return false;
53
0
  }
54
0
  if (type == LogicalTypeId::INVALID || type == LogicalTypeId::SQLNULL) {
55
0
    SetType(type_p);
56
0
    return true;
57
0
  }
58
0
  if (type_p == LogicalTypeId::SQLNULL || type == type_p) {
59
0
    return true;
60
0
  }
61
0
  if (LogicalType::IsNumeric(type) && LogicalType::IsNumeric(type_p)) {
62
0
    SetType(LogicalType::DefaultForceMaxLogicalType(LogicalType(type), LogicalType(type_p)).id());
63
0
    return true;
64
0
  }
65
0
  MarkIncompatible(doc);
66
0
  return false;
67
0
}
68
69
0
void StructureNode::SetType(LogicalTypeId type_p) {
70
0
  D_ASSERT(!incompatible && type_p != LogicalTypeId::INVALID);
71
0
  if (type == type_p) {
72
0
    return;
73
0
  }
74
0
  type = type_p;
75
0
  if (type == LogicalTypeId::LIST) {
76
0
    yyjson_mut_set_arr(&value.get());
77
0
  } else {
78
0
    const auto type_name = type == LogicalTypeId::STRUCT ? LogicalType::JSON_TYPE_NAME : EnumUtil::ToChars(type);
79
0
    yyjson_mut_set_str(&value.get(), type_name);
80
0
  }
81
0
}
82
83
0
void StructureNode::MarkIncompatible(yyjson_mut_doc *doc) {
84
0
  optional_ptr<yyjson_mut_val> fallback = yyjson_mut_str(doc, LogicalType::JSON_TYPE_NAME);
85
0
  if (!fallback) {
86
0
    throw OutOfMemoryException("Failed to allocate JSON structure value");
87
0
  }
88
0
  incompatible = true;
89
0
  array_child = nullptr;
90
0
  key_map.clear();
91
  // Replace the payload while preserving the link to the next sibling.
92
0
  value.get().tag = fallback->tag;
93
0
  value.get().uni = fallback->uni;
94
0
}
95
96
//===--------------------------------------------------------------------===//
97
// Node creation and lookup
98
//===--------------------------------------------------------------------===//
99
using NodeStorage = vector<unique_ptr<StructureNode>>;
100
101
0
StructureNode &CreateStructureNode(NodeStorage &nodes, yyjson_mut_doc *doc) {
102
0
  optional_ptr<yyjson_mut_val> value = yyjson_mut_str(doc, JSONCommon::TYPE_STRING_NULL);
103
0
  if (!value) {
104
0
    throw OutOfMemoryException("Failed to allocate JSON structure value");
105
0
  }
106
0
  nodes.push_back(make_uniq<StructureNode>(*value));
107
0
  return *nodes.back();
108
0
}
109
110
0
StructureNode &GetOrCreateArrayChild(NodeStorage &nodes, StructureNode &node, yyjson_mut_doc *doc) {
111
0
  D_ASSERT(node.type == LogicalTypeId::LIST && !node.incompatible);
112
  // All array elements contribute to one shared schema node.
113
0
  if (!node.array_child) {
114
0
    node.array_child = CreateStructureNode(nodes, doc);
115
0
    yyjson_mut_arr_append(&node.value.get(), &node.array_child->value.get());
116
0
  }
117
0
  return *node.array_child;
118
0
}
119
120
0
StructureNode &GetOrCreateObjectChild(NodeStorage &nodes, StructureNode &node, yyjson_val &key, yyjson_mut_doc *doc) {
121
0
  D_ASSERT(node.type == LogicalTypeId::STRUCT && !node.incompatible);
122
0
  const JSONKey lookup_key {unsafe_yyjson_get_str(&key), unsafe_yyjson_get_len(&key)};
123
0
  const auto entry = node.key_map.find(lookup_key);
124
0
  if (entry != node.key_map.end()) {
125
0
    return entry->second.get();
126
0
  }
127
128
0
  optional_ptr<yyjson_mut_val> output_key = yyjson_mut_strncpy(doc, lookup_key.ptr, lookup_key.len);
129
0
  if (!output_key) {
130
0
    throw OutOfMemoryException("Failed to allocate JSON structure key");
131
0
  }
132
0
  auto &child = CreateStructureNode(nodes, doc);
133
  // Empty objects keep the JSON fallback until their first field.
134
0
  if (node.key_map.empty()) {
135
0
    yyjson_mut_set_obj(&node.value.get());
136
0
  }
137
0
  yyjson_mut_obj_add(&node.value.get(), output_key.get(), &child.value.get());
138
0
  const JSONKey persistent_key {yyjson_mut_get_str(output_key.get()), lookup_key.len};
139
0
  node.key_map.emplace(persistent_key, child);
140
0
  return child;
141
0
}
142
143
//===--------------------------------------------------------------------===//
144
// Traversal
145
//===--------------------------------------------------------------------===//
146
//! Each frame tracks one input value; multiple frames can reuse the same structure node.
147
struct Frame {
148
0
  Frame(optional_ptr<yyjson_val> val_p, StructureNode &node_p) : val(val_p), node(node_p) {
149
0
  }
150
  //! The input JSON value being processed.
151
  optional_ptr<yyjson_val> val;
152
  //! The schema node to which this input value contributes.
153
  reference<StructureNode> node;
154
  //! Once initialized, the input value's container type determines the active iterator.
155
  union {
156
    yyjson_arr_iter arr_it;
157
    yyjson_obj_iter obj_it;
158
  };
159
  bool initialized = false;
160
};
161
162
0
optional<Frame> HandleArrayFrame(NodeStorage &nodes, Frame &frame, yyjson_mut_doc *doc) {
163
0
  D_ASSERT(yyjson_is_arr(frame.val.get()));
164
0
  auto &node = frame.node.get();
165
0
  if (node.incompatible) {
166
0
    return nullopt;
167
0
  }
168
169
0
  if (!frame.initialized) {
170
0
    if (!node.MergeType(LogicalTypeId::LIST, doc)) {
171
0
      return nullopt;
172
0
    }
173
174
0
    frame.arr_it = yyjson_arr_iter_with(frame.val.get());
175
0
    frame.initialized = true;
176
0
  }
177
0
  auto &child = GetOrCreateArrayChild(nodes, node, doc);
178
0
  if (child.incompatible) {
179
0
    return nullopt;
180
0
  }
181
182
  // Process scalar values without creating child frames.
183
0
  while (optional_ptr<yyjson_val> next = yyjson_arr_iter_next(&frame.arr_it)) {
184
0
    if (yyjson_is_ctn(next.get())) {
185
0
      return Frame(next, child);
186
0
    }
187
0
    if (!child.MergeType(JSONCommon::ValTypeToLogicalTypeId(next.get()), doc)) {
188
0
      return nullopt;
189
0
    }
190
0
  }
191
0
  return nullopt;
192
0
}
193
194
0
optional<Frame> HandleObjectFrame(NodeStorage &nodes, Frame &frame, yyjson_mut_doc *doc) {
195
0
  D_ASSERT(yyjson_is_obj(frame.val.get()));
196
0
  auto &node = frame.node.get();
197
0
  if (node.incompatible) {
198
0
    return nullopt;
199
0
  }
200
201
0
  if (!frame.initialized) {
202
0
    if (!node.MergeType(LogicalTypeId::STRUCT, doc)) {
203
0
      return nullopt;
204
0
    }
205
206
0
    frame.obj_it = yyjson_obj_iter_with(frame.val.get());
207
0
    frame.initialized = true;
208
0
  }
209
210
  // Process scalar values without creating child frames.
211
0
  while (optional_ptr<yyjson_val> key = yyjson_obj_iter_next(&frame.obj_it)) {
212
0
    auto &child = GetOrCreateObjectChild(nodes, node, *key, doc);
213
0
    if (child.incompatible) {
214
0
      continue;
215
0
    }
216
0
    const auto val = yyjson_obj_iter_get_val(key.get());
217
0
    if (yyjson_is_ctn(val)) {
218
0
      return Frame(val, child);
219
0
    }
220
0
    child.MergeType(JSONCommon::ValTypeToLogicalTypeId(val), doc);
221
0
  }
222
0
  return nullopt;
223
0
}
224
225
0
void HandleValueFrame(Frame &frame, yyjson_mut_doc *doc) {
226
0
  const auto val = frame.val.get();
227
0
  D_ASSERT(!yyjson_is_arr(val) && !yyjson_is_obj(val));
228
0
  frame.node.get().MergeType(JSONCommon::ValTypeToLogicalTypeId(val), doc);
229
0
}
230
231
0
yyjson_mut_val *ExtractStructure(yyjson_val *val, yyjson_mut_doc *doc) {
232
0
  NodeStorage nodes;
233
0
  vector<Frame> stack;
234
0
  stack.reserve(64);
235
236
0
  auto &root = CreateStructureNode(nodes, doc);
237
0
  stack.emplace_back(val, root);
238
239
0
  while (!stack.empty()) {
240
0
    auto &frame = stack.back();
241
242
0
    switch (yyjson_get_tag(frame.val.get())) {
243
0
    case YYJSON_TYPE_ARR | YYJSON_SUBTYPE_NONE: {
244
0
      auto child = HandleArrayFrame(nodes, frame, doc);
245
0
      if (child) {
246
0
        stack.push_back(*child);
247
0
      } else {
248
0
        stack.pop_back();
249
0
      }
250
0
      break;
251
0
    }
252
0
    case YYJSON_TYPE_OBJ | YYJSON_SUBTYPE_NONE: {
253
0
      auto child = HandleObjectFrame(nodes, frame, doc);
254
0
      if (child) {
255
0
        stack.push_back(*child);
256
0
      } else {
257
0
        stack.pop_back();
258
0
      }
259
0
      break;
260
0
    }
261
0
    default: {
262
0
      HandleValueFrame(frame, doc);
263
0
      stack.pop_back();
264
0
      break;
265
0
    }
266
0
    }
267
0
  }
268
0
  return &root.value.get();
269
0
}
270
271
//===--------------------------------------------------------------------===//
272
// SQL function
273
//===--------------------------------------------------------------------===//
274
0
optional<string_t> JSONStructureFunction(yyjson_val *val, yyjson_alc *alc, Vector &) {
275
0
  optional_ptr<yyjson_mut_doc> doc = yyjson_mut_doc_new(alc);
276
0
  if (!doc) {
277
0
    throw OutOfMemoryException("Failed to allocate JSON structure document");
278
0
  }
279
0
  return JSONCommon::WriteVal<yyjson_mut_val>(ExtractStructure(val, doc.get()), alc);
280
0
}
281
282
0
void StructureFunction(DataChunk &args, ExpressionState &state, Vector &result) {
283
0
  JSONExecutors::UnaryExecute<string_t>(args, state, result, JSONStructureFunction);
284
0
}
285
286
29.7k
ScalarFunction GetStructureFunctionInternal(const LogicalType &input_type) {
287
29.7k
  ScalarFunction fun({}, LogicalType::JSON(), StructureFunction, nullptr, nullptr, JSONFunctionLocalState::Init);
288
29.7k
  fun.GetSignature().AddParameter("json", input_type);
289
29.7k
  return fun;
290
29.7k
}
291
292
} // namespace
293
294
14.8k
ScalarFunctionSet JSONFunctions::GetStructureFunction() {
295
14.8k
  ScalarFunctionSet fun_set("json_structure");
296
297
14.8k
  fun_set.AddFunction(GetStructureFunctionInternal(LogicalType::VARCHAR));
298
14.8k
  fun_set.AddFunction(GetStructureFunctionInternal(LogicalType::JSON()));
299
14.8k
  fun_set.SetFallible();
300
301
14.8k
  return fun_set;
302
14.8k
}
303
304
} // namespace duckdb