TODO(IMPALA-7610): parse the JSON path and cache it so we don't need to parse it everytime
| 305 | /// TODO(IMPALA-7610): parse the JSON path and cache it so we don't need to parse it |
| 306 | /// everytime |
| 307 | StringVal StringFunctions::GetJsonObjectImpl(FunctionContext* ctx, |
| 308 | const StringVal& json_str, const StringVal& path_str) { |
| 309 | if (UNLIKELY(json_str.is_null || json_str.len == 0)) return StringVal::null(); |
| 310 | if (UNLIKELY(path_str.is_null || path_str.len == 0)) { |
| 311 | ctx->SetError("Empty json path"); |
| 312 | return StringVal::null(); |
| 313 | } |
| 314 | int beg = 0; |
| 315 | // Strip off preceding whitespace. |
| 316 | while (beg < path_str.len && path_str.ptr[beg] == ' ') beg++; |
| 317 | if (UNLIKELY(beg == path_str.len || path_str.ptr[beg] != '$')) { |
| 318 | // Here we use '$$' to escape '$' in Substitute |
| 319 | string msg = Substitute("Failed to parse json path '$0': Should start with '$$'", |
| 320 | AnyValUtil::ToString(path_str)); |
| 321 | ctx->SetError(msg.c_str()); |
| 322 | return StringVal::null(); |
| 323 | } |
| 324 | |
| 325 | JsonUdfAllocator allocator(ctx); |
| 326 | JsonUdfDocument document(&allocator); |
| 327 | if (!ParseStringVal(ctx, json_str, &document)) return StringVal::null(); |
| 328 | |
| 329 | // BFS to extract selected values. We use array of RapidJson instead of std::vector to |
| 330 | // track its memory. |
| 331 | JsonUdfValue queue(kArrayType); |
| 332 | RETURN_NULL_IF_OOM(queue.Reserve(INITIAL_QUEUE_CAPACITY, allocator)); |
| 333 | RETURN_NULL_IF_OOM(queue.PushBack(document, allocator)); |
| 334 | const uint8_t* path = path_str.ptr; |
| 335 | const uint8_t* path_end = path + path_str.len; |
| 336 | for (int i = beg + 1; i < path_str.len;) { |
| 337 | // Each round we extract new items into the queue. Old items will be removed. |
| 338 | switch (path[i]) { |
| 339 | case '$': { |
| 340 | string msg = Substitute("Failed to parse json path '$0':" |
| 341 | " $$ should only be placed at start", AnyValUtil::ToString(path_str)); |
| 342 | ctx->SetError(msg.c_str()); |
| 343 | return StringVal::null(); |
| 344 | } |
| 345 | case '.': { |
| 346 | // Hive does not skip the heading and trailing whitespaces since it simply splits |
| 347 | // the json path by '.'. We should keep the same behavior with MySQL. See |
| 348 | // JSON_EXTRACT in MySQL(5.7+). |
| 349 | for (++i; i < path_str.len && path[i] == ' '; ++i); // skip whitespaces |
| 350 | if (i == path_str.len) { |
| 351 | string msg = Substitute("Failed to parse json path '$0': Found a trailing '.'", |
| 352 | AnyValUtil::ToString(path_str)); |
| 353 | ctx->SetError(msg.c_str()); |
| 354 | return StringVal::null(); |
| 355 | } |
| 356 | if (path[i] == '*') { |
| 357 | i = ProcessWildcardKey(ctx, path_str, ++i, &queue, &allocator); |
| 358 | if (i < 0) return StringVal::null(); |
| 359 | break; |
| 360 | } |
| 361 | const uint8_t* start = path + i; |
| 362 | const uint8_t* end = FindEndOfIdentifier(start, path_end); |
| 363 | // Set error if looking for an empty key |
| 364 | if (end == nullptr) { |
nothing calls this directly
no test coverage detected