diff --git a/docs/en/sql-reference/functions/json-functions.md b/docs/en/sql-reference/functions/json-functions.md index 1c82506a7539..0881da51fa48 100644 --- a/docs/en/sql-reference/functions/json-functions.md +++ b/docs/en/sql-reference/functions/json-functions.md @@ -843,6 +843,198 @@ JSONExtractRaw(json [, indices_or_keys]...) SELECT JSONExtractRaw('{"a": "hello", "b": [-100, 200.0, 300]}', 'b') = '[-100, 200.0, 300]'; ``` +### Case-Insensitive JSONExtract Functions + +The following functions perform case-insensitive key matching when extracting values from JSON objects. They work identically to their case-sensitive counterparts, except that object keys are matched without regard to case. + +> These functions may be less performant than their case-sensitive counterparts, so use the regular JSONExtract functions if possible. + +### JSONExtractIntCaseInsensitive {#jsonextractintcaseinsensitive} + +Parses JSON and extracts a value of Int type using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractIntCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractIntCaseInsensitive('{"Value": 123}', 'value') = 123 +SELECT JSONExtractIntCaseInsensitive('{"VALUE": -456}', 'Value') = -456 +``` + +### JSONExtractUIntCaseInsensitive {#jsonextractuintcaseinsensitive} + +Parses JSON and extracts a value of UInt type using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractUIntCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractUIntCaseInsensitive('{"COUNT": 789}', 'count') = 789 +``` + +### JSONExtractFloatCaseInsensitive {#jsonextractfloatcaseinsensitive} + +Parses JSON and extracts a value of Float type using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractFloatCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractFloatCaseInsensitive('{"Price": 12.34}', 'PRICE') = 12.34 +``` + +### JSONExtractBoolCaseInsensitive {#jsonextractboolcaseinsensitive} + +Parses JSON and extracts a boolean value using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractBoolCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractBoolCaseInsensitive('{"IsActive": true}', 'isactive') = 1 +``` + +### JSONExtractStringCaseInsensitive {#jsonextractstringcaseinsensitive} + +Parses JSON and extracts a string using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractStringCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractStringCaseInsensitive('{"ABC": "def"}', 'abc') = 'def' +SELECT JSONExtractStringCaseInsensitive('{"User": {"Name": "John"}}', 'user', 'name') = 'John' +``` + +### JSONExtractCaseInsensitive {#jsonextractcaseinsensitive} + +Parses JSON and extracts a value of the given ClickHouse data type using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractCaseInsensitive(json [, indices_or_keys...], return_type) +``` + +**Example** + +```sql +SELECT JSONExtractCaseInsensitive('{"Number": 123}', 'number', 'Int32') = 123 +SELECT JSONExtractCaseInsensitive('{"List": [1, 2, 3]}', 'list', 'Array(Int32)') = [1, 2, 3] +``` + +### JSONExtractKeysAndValuesCaseInsensitive {#jsonextractkeysandvaluescaseinsensitive} + +Parses key-value pairs from JSON using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractKeysAndValuesCaseInsensitive(json [, indices_or_keys...], value_type) +``` + +**Example** + +```sql +SELECT JSONExtractKeysAndValuesCaseInsensitive('{"Name": "Alice", "AGE": 30}', 'String')[1] = ('Name', 'Alice') +``` + +### JSONExtractRawCaseInsensitive {#jsonextractrawcaseinsensitive} + +Returns part of the JSON as an unparsed string using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractRawCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractRawCaseInsensitive('{"Object": {"key": "value"}}', 'OBJECT') = '{"key":"value"}' +``` + +### JSONExtractArrayRawCaseInsensitive {#jsonextractarrayrawcaseinsensitive} + +Returns an array with elements of JSON array, each represented as unparsed string, using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractArrayRawCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractArrayRawCaseInsensitive('{"Items": [1, 2, 3]}', 'ITEMS') = ['1', '2', '3'] +``` + +### JSONExtractKeysAndValuesRawCaseInsensitive {#jsonextractkeysandvaluesrawcaseinsensitive} + +Extracts raw key-value pairs from JSON using case-insensitive key matching. + +**Syntax** + +```sql +JSONExtractKeysAndValuesRawCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractKeysAndValuesRawCaseInsensitive('{"Name": "Alice", "AGE": 30}')[1] = ('Name', '"Alice"') +``` + +### JSONExtractKeysCaseInsensitive {#jsonextractkeyescaseinsensitive} + +Parses a JSON string and extracts the keys using case-insensitive key matching to navigate to nested objects. + +**Syntax** + +```sql +JSONExtractKeysCaseInsensitive(json [, indices_or_keys]...) +``` + +**Example** + +```sql +SELECT JSONExtractKeysCaseInsensitive('{"Name": "Alice", "AGE": 30}') = ['Name', 'AGE'] +SELECT JSONExtractKeysCaseInsensitive('{"User": {"name": "John", "AGE": 25}}', 'user') = ['name', 'AGE'] +``` + +**Implementation Notes** + +- When multiple keys match with different cases, the first match is returned +- Case-insensitive matching only applies to object keys, not to array indices or the extracted values +- The comparison is ASCII case-insensitive + ### JSONExtractArrayRaw {#jsonextractarrayraw} Returns an array with elements of JSON array, each represented as unparsed string. If the part does not exist or isn't an array, then an empty array will be returned. diff --git a/src/Common/JSONParsers/DummyJSONParser.h b/src/Common/JSONParsers/DummyJSONParser.h index 647fe3083a1f..19d64fc39e10 100644 --- a/src/Common/JSONParsers/DummyJSONParser.h +++ b/src/Common/JSONParsers/DummyJSONParser.h @@ -82,6 +82,7 @@ struct DummyJSONParser static Iterator end() { return {}; } static size_t size() { return 0; } bool find(std::string_view, Element &) const { return false; } /// NOLINT + bool findCaseInsensitive(std::string_view, Element &) const { return false; } /// NOLINT #if 0 /// Optional: Provides access to an object's element by index. diff --git a/src/Common/JSONParsers/RapidJSONParser.h b/src/Common/JSONParsers/RapidJSONParser.h index 0ead4f721cfe..2c14f540233c 100644 --- a/src/Common/JSONParsers/RapidJSONParser.h +++ b/src/Common/JSONParsers/RapidJSONParser.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace DB { @@ -126,6 +127,33 @@ struct RapidJSONParser return true; } + bool findCaseInsensitive(std::string_view key, Element & result) const + { + // RapidJSON doesn't have native case-insensitive search, so we iterate + for (auto it = ptr->MemberBegin(); it != ptr->MemberEnd(); ++it) + { + std::string_view member_key(it->name.GetString(), it->name.GetStringLength()); + if (member_key.size() == key.size()) + { + bool match = true; + for (size_t i = 0; i < key.size(); ++i) + { + if (!equalsCaseInsensitive(member_key[i], key[i])) + { + match = false; + break; + } + } + if (match) + { + result = it->value; + return true; + } + } + } + return false; + } + /// Optional: Provides access to an object's element by index. ALWAYS_INLINE KeyValuePair operator[](size_t index) const { diff --git a/src/Common/JSONParsers/SimdJSONParser.h b/src/Common/JSONParsers/SimdJSONParser.h index a39991aeba9d..4be972b9f613 100644 --- a/src/Common/JSONParsers/SimdJSONParser.h +++ b/src/Common/JSONParsers/SimdJSONParser.h @@ -376,6 +376,16 @@ struct SimdJSONParser return true; } + bool findCaseInsensitive(std::string_view key, Element & result) const + { + auto x = object.at_key_case_insensitive(key); + if (x.error()) + return false; + + result = x.value_unsafe(); + return true; + } + /// Optional: Provides access to an object's element by index. KeyValuePair operator[](size_t index) const { diff --git a/src/Functions/FunctionsJSON.cpp b/src/Functions/FunctionsJSON.cpp index fc7eeb25b391..17d5d036df88 100644 --- a/src/Functions/FunctionsJSON.cpp +++ b/src/Functions/FunctionsJSON.cpp @@ -86,7 +86,7 @@ concept Preparable = requires (T t) class FunctionJSONHelpers { public: - template typename Impl, class JSONParser> + template typename Impl, class JSONParser, bool case_insensitive = false> class Executor { public: @@ -158,7 +158,7 @@ class FunctionJSONHelpers /// Perform moves. Element element; std::string_view last_key; - bool moves_ok = performMoves(arguments, i, document, moves, element, last_key); + bool moves_ok = performMoves(arguments, i, document, moves, element, last_key); if (moves_ok) added_to_column = impl.insertResultToColumn(*to, element, last_key, format_settings, error); @@ -232,7 +232,7 @@ class FunctionJSONHelpers /// Performs moves of types MoveType::Index and MoveType::ConstIndex. - template + template static bool performMoves(const ColumnsWithTypeAndName & arguments, size_t row, const typename JSONParser::Element & document, const std::vector & moves, typename JSONParser::Element & element, std::string_view & last_key) @@ -253,8 +253,16 @@ class FunctionJSONHelpers case MoveType::ConstKey: { key = moves[j].key; - if (!moveToElementByKey(res_element, key)) - return false; + if constexpr (case_insensitive) + { + if (!moveToElementByKeyCaseInsensitive(res_element, key)) + return false; + } + else + { + if (!moveToElementByKey(res_element, key)) + return false; + } break; } case MoveType::Index: @@ -267,8 +275,16 @@ class FunctionJSONHelpers case MoveType::Key: { key = arguments[j + 1].column->getDataAt(row).toView(); - if (!moveToElementByKey(res_element, key)) - return false; + if constexpr (case_insensitive) + { + if (!moveToElementByKeyCaseInsensitive(res_element, key)) + return false; + } + else + { + if (!moveToElementByKey(res_element, key)) + return false; + } break; } } @@ -327,6 +343,16 @@ class FunctionJSONHelpers return object.find(key, element); } + /// Performs case-insensitive moves of types MoveType::Key and MoveType::ConstKey. + template + static bool moveToElementByKeyCaseInsensitive(typename JSONParser::Element & element, std::string_view key) + { + if (!element.isObject()) + return false; + auto object = element.getObject(); + return object.findCaseInsensitive(key, element); + } + static size_t calculateMaxSize(const ColumnString::Offsets & offsets) { size_t max_size = 0; @@ -363,7 +389,7 @@ constexpr bool functionForcesTheReturnType() return std::is_same_v, JSONExtractImpl> || std::is_same_v, JSONExtractKeysAndValuesImpl>; } -template typename Impl> +template typename Impl, bool case_insensitive = false> class ExecutableFunctionJSON : public IExecutableFunction { @@ -434,13 +460,13 @@ class ExecutableFunctionJSON : public IExecutableFunction { #if USE_SIMDJSON if (allow_simdjson) - return FunctionJSONHelpers::Executor::run(arguments, result_type, input_rows_count, format_settings); + return FunctionJSONHelpers::Executor::run(arguments, result_type, input_rows_count, format_settings); #endif #if USE_RAPIDJSON - return FunctionJSONHelpers::Executor::run(arguments, result_type, input_rows_count, format_settings); + return FunctionJSONHelpers::Executor::run(arguments, result_type, input_rows_count, format_settings); #else - return FunctionJSONHelpers::Executor::run(arguments, result_type, input_rows_count, format_settings); + return FunctionJSONHelpers::Executor::run(arguments, result_type, input_rows_count, format_settings); #endif } @@ -451,7 +477,7 @@ class ExecutableFunctionJSON : public IExecutableFunction }; -template typename Impl> +template typename Impl, bool case_insensitive = false> class FunctionBaseFunctionJSON : public IFunctionBase { public: @@ -487,7 +513,7 @@ class FunctionBaseFunctionJSON : public IFunctionBase ExecutableFunctionPtr prepare(const ColumnsWithTypeAndName &) const override { - return std::make_unique>(null_presence, allow_simdjson, json_return_type, format_settings); + return std::make_unique>(null_presence, allow_simdjson, json_return_type, format_settings); } private: @@ -501,7 +527,7 @@ class FunctionBaseFunctionJSON : public IFunctionBase /// We use IFunctionOverloadResolver instead of IFunction to handle non-default NULL processing. /// Both NULL and JSON NULL should generate NULL value. If any argument is NULL, return NULL. -template typename Impl> +template typename Impl, bool case_insensitive = false> class JSONOverloadResolver : public IFunctionOverloadResolver, WithContext { public: @@ -546,7 +572,7 @@ class JSONOverloadResolver : public IFunctionOverloadResolver, WithContext argument_types.reserve(arguments.size()); for (const auto & argument : arguments) argument_types.emplace_back(argument.type); - return std::make_unique>( + return std::make_unique>( null_presence, getContext()->getSettingsRef()[Setting::allow_simdjson], argument_types, return_type, json_return_type, getFormatSettings(getContext())); } }; @@ -568,6 +594,19 @@ struct NameJSONExtractArrayRaw { static constexpr auto name{"JSONExtractArrayRaw struct NameJSONExtractKeysAndValuesRaw { static constexpr auto name{"JSONExtractKeysAndValuesRaw"}; }; struct NameJSONExtractKeys { static constexpr auto name{"JSONExtractKeys"}; }; +// Case-insensitive variants +struct NameJSONExtractIntCaseInsensitive { static constexpr auto name{"JSONExtractIntCaseInsensitive"}; }; +struct NameJSONExtractUIntCaseInsensitive { static constexpr auto name{"JSONExtractUIntCaseInsensitive"}; }; +struct NameJSONExtractFloatCaseInsensitive { static constexpr auto name{"JSONExtractFloatCaseInsensitive"}; }; +struct NameJSONExtractBoolCaseInsensitive { static constexpr auto name{"JSONExtractBoolCaseInsensitive"}; }; +struct NameJSONExtractStringCaseInsensitive { static constexpr auto name{"JSONExtractStringCaseInsensitive"}; }; +struct NameJSONExtractCaseInsensitive { static constexpr auto name{"JSONExtractCaseInsensitive"}; }; +struct NameJSONExtractKeysAndValuesCaseInsensitive { static constexpr auto name{"JSONExtractKeysAndValuesCaseInsensitive"}; }; +struct NameJSONExtractRawCaseInsensitive { static constexpr auto name{"JSONExtractRawCaseInsensitive"}; }; +struct NameJSONExtractArrayRawCaseInsensitive { static constexpr auto name{"JSONExtractArrayRawCaseInsensitive"}; }; +struct NameJSONExtractKeysAndValuesRawCaseInsensitive { static constexpr auto name{"JSONExtractKeysAndValuesRawCaseInsensitive"}; }; +struct NameJSONExtractKeysCaseInsensitive { static constexpr auto name{"JSONExtractKeysCaseInsensitive"}; }; + template class JSONHasImpl @@ -1095,6 +1134,19 @@ REGISTER_FUNCTION(JSON) factory.registerFunction>(); factory.registerFunction>(); factory.registerFunction>(); + + // Register case-insensitive variants + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); + factory.registerFunction>(); } } diff --git a/tests/queries/0_stateless/03298_json_extract_case_insensitive.reference b/tests/queries/0_stateless/03298_json_extract_case_insensitive.reference new file mode 100644 index 000000000000..92a5af7ffd0a --- /dev/null +++ b/tests/queries/0_stateless/03298_json_extract_case_insensitive.reference @@ -0,0 +1,38 @@ +--JSONExtractCaseInsensitive-- +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +--allow_simdjson=0-- +1 +1 +1 +--allow_simdjson=1-- +1 +1 +1 \ No newline at end of file diff --git a/tests/queries/0_stateless/03298_json_extract_case_insensitive.sql b/tests/queries/0_stateless/03298_json_extract_case_insensitive.sql new file mode 100644 index 000000000000..519caa41b058 --- /dev/null +++ b/tests/queries/0_stateless/03298_json_extract_case_insensitive.sql @@ -0,0 +1,71 @@ +-- Tags: no-fasttest +-- Tag: no-fasttest due to only SIMD JSON is available in fasttest + +SELECT '--JSONExtractCaseInsensitive--'; + +-- Basic case-insensitive key matching +SELECT JSONExtractStringCaseInsensitive('{"ABC": "def"}', 'abc') = 'def'; +SELECT JSONExtractStringCaseInsensitive('{"abc": "def"}', 'ABC') = 'def'; +SELECT JSONExtractStringCaseInsensitive('{"AbC": "def"}', 'aBc') = 'def'; +SELECT JSONExtractStringCaseInsensitive('{"abc": "def", "ABC": "ghi"}', 'abc') = 'def'; -- Should return first match + +-- Different data types +SELECT JSONExtractIntCaseInsensitive('{"Value": 123}', 'value') = 123; +SELECT JSONExtractIntCaseInsensitive('{"VALUE": -456}', 'Value') = -456; +SELECT JSONExtractUIntCaseInsensitive('{"COUNT": 789}', 'count') = 789; +SELECT JSONExtractFloatCaseInsensitive('{"Price": 12.34}', 'PRICE') = 12.34; +SELECT JSONExtractBoolCaseInsensitive('{"IsActive": true}', 'isactive') = 1; + +-- Nested objects +SELECT JSONExtractStringCaseInsensitive('{"User": {"Name": "John"}}', 'user', 'name') = 'John'; +SELECT JSONExtractIntCaseInsensitive('{"DATA": {"COUNT": 42}}', 'data', 'Count') = 42; + +-- Arrays +SELECT JSONExtractIntCaseInsensitive('{"Items": [1, 2, 3]}', 'items', 1) = 2; +SELECT JSONExtractStringCaseInsensitive('{"TAGS": ["a", "b", "c"]}', 'tags', 0) = 'a'; + +-- Raw extraction +SELECT JSONExtractRawCaseInsensitive('{"Object": {"key": "value"}}', 'OBJECT') = '{"key":"value"}'; +SELECT JSONExtractRawCaseInsensitive('{"Array": [1, 2, 3]}', 'array') = '[1,2,3]'; + +-- Generic extraction with type +SELECT JSONExtractCaseInsensitive('{"Number": 123}', 'number', 'Int32') = 123; +SELECT JSONExtractCaseInsensitive('{"Text": "hello"}', 'TEXT', 'String') = 'hello'; +SELECT JSONExtractCaseInsensitive('{"List": [1, 2, 3]}', 'list', 'Array(Int32)') = [1, 2, 3]; + +-- Keys and values extraction +SELECT JSONExtractKeysAndValuesCaseInsensitive('{"Name": "Alice", "AGE": 30}', 'String')[1] = ('Name', 'Alice'); +SELECT JSONExtractKeysAndValuesCaseInsensitive('{"ID": 1, "Value": 2}', 'Int32')[2] = ('Value', 2); + +-- Non-existent keys +SELECT JSONExtractStringCaseInsensitive('{"abc": "def"}', 'xyz') IS NULL; +SELECT JSONExtractIntCaseInsensitive('{"abc": 123}', 'XYZ') = 0; + +-- Empty JSON +SELECT JSONExtractStringCaseInsensitive('{}', 'key') IS NULL; + +-- Multiple keys with different cases (should return the first match) +SELECT JSONExtractStringCaseInsensitive('{"key": "first", "KEY": "second", "Key": "third"}', 'KEY') = 'first'; + +-- Complex nested example +SELECT JSONExtractIntCaseInsensitive('{"LEVEL1": {"Level2": {"level3": 999}}}', 'level1', 'LEVEL2', 'LEVEL3') = 999; + +-- Additional functions: ArrayRaw, KeysAndValuesRaw, Keys +SELECT JSONExtractArrayRawCaseInsensitive('{"Items": [1, 2, 3]}', 'ITEMS') = ['1','2','3']; +SELECT JSONExtractKeysAndValuesRawCaseInsensitive('{"Name": "Alice", "AGE": 30}')[1] = ('Name', '"Alice"'); +SELECT JSONExtractKeysCaseInsensitive('{"Name": "Alice", "AGE": 30}') = ['Name', 'AGE']; + +-- Testing with both allow_simdjson settings +SELECT '--allow_simdjson=0--'; +SET allow_simdjson=0; + +SELECT JSONExtractStringCaseInsensitive('{"ABC": "def"}', 'abc') = 'def'; +SELECT JSONExtractIntCaseInsensitive('{"Value": 123}', 'value') = 123; +SELECT JSONExtractFloatCaseInsensitive('{"Price": 12.34}', 'PRICE') = 12.34; + +SELECT '--allow_simdjson=1--'; +SET allow_simdjson=1; + +SELECT JSONExtractStringCaseInsensitive('{"ABC": "def"}', 'abc') = 'def'; +SELECT JSONExtractIntCaseInsensitive('{"Value": 123}', 'value') = 123; +SELECT JSONExtractFloatCaseInsensitive('{"Price": 12.34}', 'PRICE') = 12.34; \ No newline at end of file diff --git a/tests/queries/0_stateless/03299_json_extract_case_insensitive_edge_cases.reference b/tests/queries/0_stateless/03299_json_extract_case_insensitive_edge_cases.reference new file mode 100644 index 000000000000..18aeb6362fcf --- /dev/null +++ b/tests/queries/0_stateless/03299_json_extract_case_insensitive_edge_cases.reference @@ -0,0 +1,25 @@ +--Edge cases for JSONExtractCaseInsensitive-- +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 \ No newline at end of file diff --git a/tests/queries/0_stateless/03299_json_extract_case_insensitive_edge_cases.sql b/tests/queries/0_stateless/03299_json_extract_case_insensitive_edge_cases.sql new file mode 100644 index 000000000000..911c12c92432 --- /dev/null +++ b/tests/queries/0_stateless/03299_json_extract_case_insensitive_edge_cases.sql @@ -0,0 +1,68 @@ +-- Tags: no-fasttest +-- Tag: no-fasttest due to only SIMD JSON is available in fasttest + +SELECT '--Edge cases for JSONExtractCaseInsensitive--'; + +-- Keys with special characters +SELECT JSONExtractStringCaseInsensitive('{"key-with-dash": "value1"}', 'KEY-WITH-DASH') = 'value1'; +SELECT JSONExtractStringCaseInsensitive('{"key_with_underscore": "value2"}', 'KEY_WITH_UNDERSCORE') = 'value2'; +SELECT JSONExtractStringCaseInsensitive('{"key.with.dots": "value3"}', 'KEY.WITH.DOTS') = 'value3'; +SELECT JSONExtractStringCaseInsensitive('{"key with spaces": "value4"}', 'KEY WITH SPACES') = 'value4'; + +-- Unicode keys (ASCII case-insensitive only) +SELECT JSONExtractStringCaseInsensitive('{"café": "coffee"}', 'CAFÉ') IS NULL; -- Should not match (non-ASCII) +SELECT JSONExtractStringCaseInsensitive('{"café": "coffee"}', 'café') = 'coffee'; -- Exact match works + +-- Numeric string keys +SELECT JSONExtractStringCaseInsensitive('{"123": "numeric key"}', '123') = 'numeric key'; + +-- Empty string key +SELECT JSONExtractStringCaseInsensitive('{"": "empty key"}', '') = 'empty key'; + +-- Very long keys +SELECT JSONExtractStringCaseInsensitive( + concat('{"', repeat('VeryLongKey', 100), '": "value"}'), + repeat('verylongkey', 100) +) = 'value'; + +-- Mixed types +SELECT JSONExtractStringCaseInsensitive('{"Key": 123}', 'key') = '123'; -- Number as string +SELECT JSONExtractStringCaseInsensitive('{"Key": true}', 'KEY') = 'true'; -- Bool as string +SELECT JSONExtractIntCaseInsensitive('{"Key": "123"}', 'key') = 123; -- String as number +SELECT JSONExtractBoolCaseInsensitive('{"Key": 1}', 'KEY') = 1; -- Number as bool + +-- Null values +SELECT JSONExtractStringCaseInsensitive('{"Key": null}', 'key') IS NULL; +SELECT JSONExtractIntCaseInsensitive('{"Key": null}', 'KEY') = 0; + +-- Invalid JSON +SELECT JSONExtractStringCaseInsensitive('not a json', 'key') IS NULL; +SELECT JSONExtractIntCaseInsensitive('{invalid json}', 'key') = 0; + +-- Case sensitivity comparison +SELECT JSONExtractString('{"ABC": "def", "abc": "ghi"}', 'abc') = 'ghi'; -- Case sensitive - exact match +SELECT JSONExtractStringCaseInsensitive('{"ABC": "def", "abc": "ghi"}', 'abc') = 'def'; -- Case insensitive - first match + +-- Performance test with many keys +WITH json AS ( + SELECT concat('{', + arrayStringConcat( + arrayMap(i -> concat('"key', toString(i), '": ', toString(i)), + range(1000)), + ',' + ), + ', "TARGET": 999}' + ) AS str +) +SELECT JSONExtractIntCaseInsensitive(str, 'target') = 999 FROM json; + +-- Multiple levels of nesting +SELECT JSONExtractStringCaseInsensitive( + '{"LEVEL1": {"level2": {"LEVEL3": {"level4": "deep"}}}}', + 'level1', 'LEVEL2', 'level3', 'LEVEL4' +) = 'deep'; + +-- Test additional functions with case-insensitive keys +SELECT JSONExtractArrayRawCaseInsensitive('{"ARRAY": ["test", 123, true]}', 'array') = ['"test"', '123', 'true']; +SELECT length(JSONExtractKeysAndValuesRawCaseInsensitive('{"KEY1": "value1", "key2": 100}')) = 2; +SELECT JSONExtractKeysCaseInsensitive('{"ABC": 1, "def": 2, "GHI": 3}')[1] = 'ABC'; \ No newline at end of file