/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 |