diff --git a/builds/posix/Makefile.in b/builds/posix/Makefile.in index 9065dae97f8..fe0d00128d8 100644 --- a/builds/posix/Makefile.in +++ b/builds/posix/Makefile.in @@ -973,6 +973,7 @@ clean_gpre_gen: clean_yacc_gen: $(RM) $(ROOT)/src/dsql/parse.cpp $(ROOT)/src/dsql/dsql.tab.h $(TMP_ROOT)/include/gen/parse.h types.y y.* $(OBJ)/.parse-gen-sentinel + $(RM) $(ROOT)/jrd/json/path/JsonPathParser.cpp $(GEN_ROOT)/jtypes.y $(GEN_ROOT)/json_y.y $(TMP_ROOT)/include/gen/jparse.h jtypes.y y.* $(OBJ)/.jparse-gen-sentinel #___________________________________________________________________________ # Extra platform specific targets diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index 47c58cb3612..f997f75d9e8 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -61,6 +61,30 @@ $(OBJ)/.parse-gen-sentinel: $(SRC_ROOT)/dsql/parse.y $(SRC_ROOT)/dsql/btyacc_fb. touch $@ +# This rule creates JsonPathParser.cpp from JsonPathParser.y + +$(OBJ)/jrd/json/path/JsonPathParser.cpp $(SRC_ROOT)/include/gen/jparse.h: $(OBJ)/.jparse-gen-sentinel ; + +$(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_ROOT)/jrd/json/path/btyacc_json.ske + sed -n '/%type .*/p' < $< > $(GEN_ROOT)/jtypes.y + sed 's/%type .*//' < $< > $(GEN_ROOT)/json_y.y + ($(BTYACC) -l -d -b jpath -S $(SRC_ROOT)/jrd/json/path/btyacc_json.ske $(GEN_ROOT)/json_y.y; echo $$? > $(GEN_ROOT)/json_y.status) 2>&1 | tee $(GEN_ROOT)/json_y.txt + (exit `cat $(GEN_ROOT)/json_y.status`) + sed -n -e "s/.*btyacc: \(.*conflicts.*\)/\1/p" $(GEN_ROOT)/json_y.txt > $(TMP_ROOT)/jpath-parse-conflicts.txt + cp $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt + if [ ! -f $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt ] || ! cmp -s $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt; then \ + cp $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt || true; \ + fi + sed -i -e 's/#define \([A-Z].*\)/#define TOK_\1/' $(GEN_ROOT)/jpath_tab.h + sed -i -e 's/#define TOK_YY\(.*\)/#define YY\1/' $(GEN_ROOT)/jpath_tab.h + $(MV) $(GEN_ROOT)/jpath_tab.h $(SRC_ROOT)/include/gen/jparse.h + $(MV) $(GEN_ROOT)/jpath_tab.c $(OBJ)/jrd/json/path/JsonPathParser.cpp + touch $(OBJ)/jrd/json/path/JsonPathParser.cpp + +# Explicit dependence on generated header (jparser) +$(OBJ)/jrd/json/path/JsonPath.o $(OBJ)/jrd/json/path/JsonPathParser.o $(OBJ)/jrd/RecordSourceNodes.o $(OBJ)/dsql/ExprNodes.o: $(SRC_ROOT)/include/gen/jparse.h + + # gpre_meta needs a special boot build since there is no database. $(SRC_ROOT)/gpre/gpre_meta.cpp: $(SRC_ROOT)/gpre/gpre_meta.epp diff --git a/builds/posix/make.shared.variables b/builds/posix/make.shared.variables index b8cf3f1b8e0..c2b1455afe5 100644 --- a/builds/posix/make.shared.variables +++ b/builds/posix/make.shared.variables @@ -86,13 +86,18 @@ AllObjects += $(Chacha_Objects) Profiler_Objects:= $(call dirObjects,plugins/profiler) AllObjects += $(Profiler_Objects) +JSON_Objects:= $(call dirObjects,jrd/json) \ + $(call dirObjects,jrd/json/path) \ + $(call dirObjects,jrd/json/classes) + # Engine Engine_Objects:= $(call dirObjects,jrd) $(call dirObjects,dsql) $(call dirObjects,jrd/extds) \ $(call dirObjects,jrd/optimizer) $(call dirObjects,jrd/recsrc) $(call dirObjects,jrd/replication) \ $(call dirObjects,jrd/sys-packages) $(call dirObjects,jrd/trace) \ - $(call makeObjects,lock,lock.cpp) + $(call makeObjects,lock,lock.cpp) $(JSON_Objects) -Engine_Test_Objects:= $(call dirObjects,jrd/tests) $(call dirObjects,lock/tests) +Engine_Test_Objects:= $(call dirObjects,jrd/tests) $(call dirObjects,lock/tests) \ + $(call dirObjects,jrd/tests/json) $(call dirObjects,jrd/tests/json/classes) AllObjects += $(Engine_Objects) $(Engine_Test_Objects) diff --git a/builds/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj index 5b0b3ceef1c..7978923259f 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -200,6 +200,14 @@ + + + + + + + + @@ -402,6 +410,12 @@ + + + + + + @@ -422,6 +436,9 @@ + + Document + {B32D1B09-8161-451E-8D20-D30F26094EC0} diff --git a/builds/win32/msvc15/engine_static.vcxproj.filters b/builds/win32/msvc15/engine_static.vcxproj.filters index 9b99e9ebb8f..53f3935906b 100644 --- a/builds/win32/msvc15/engine_static.vcxproj.filters +++ b/builds/win32/msvc15/engine_static.vcxproj.filters @@ -46,6 +46,9 @@ {4bcf3dd7-c9c1-4148-bfeb-d5960bf35faa} + + {49f32cf0-a84a-4f19-8efd-58524e855ce2} + @@ -1214,5 +1217,23 @@ DSQL + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + diff --git a/builds/win32/parse.bat b/builds/win32/parse.bat index e2ea85fd632..1a1e92c803f 100644 --- a/builds/win32/parse.bat +++ b/builds/win32/parse.bat @@ -27,4 +27,27 @@ @del y_tab.c @del sed* + +@echo Generating JsonPathParser.cpp + +@sed -n "/%%type .*/p" < %FB_ROOT_PATH%\src\jrd\json\path\JsonPathParser.y > jtypes.y +@sed "s/%%type .*//" < %FB_ROOT_PATH%\src\jrd\json\path\JsonPathParser.y > json_y.y + +%FB_ROOT_PATH%\temp\%FB_OBJ_DIR%\btyacc\btyacc -l -d -b jpath -S %FB_ROOT_PATH%\src\jrd\json\path\btyacc_json.ske json_y.y 2>json_y.txt +@if errorlevel 1 (type json_y.txt && exit /B 1) +@type json_y.txt + +@sed -i "s/#define \([A-Z].*\)/#define TOK_\1/" jpath_tab.h +@sed -i "s/#define TOK_YY\(.*\)/#define YY\1/" jpath_tab.h +@sed -n -e "s/.*btyacc: \(.*conflicts.*\)/\1/p" json_y.txt > %FB_ROOT_PATH%\src\dsql\jpath-parse-conflicts.txt + +@copy jpath_tab.h %FB_ROOT_PATH%\src\include\gen\jparse.h > nul +@copy jpath_tab.c %FB_ROOT_PATH%\src\jrd\json\path\JsonPathParser.cpp > nul +@del json_y.y +@del json_y.txt +@del jtypes.y +@del jpath_tab.h +@del jpath_tab.c +@del sed* + :END diff --git a/doc/sql.extensions/README.json.md b/doc/sql.extensions/README.json.md new file mode 100644 index 00000000000..81918c6f72a --- /dev/null +++ b/doc/sql.extensions/README.json.md @@ -0,0 +1,199 @@ +# JSON + +Functional to parse, generate, query and store JSON + +## JSON Path + +The JSON Path is uses to query data from JSON. +For example, the path `$[*].name` with JSON `[{"name":"John", "score":10}, {"name":"Sam", "score":50}]` will produce the sequence `["John","name"]`. +To filter elements, a filer can be used: `$[*] ? (@.score > 15)`. Child filter is also supported: `$[*] ? (@.score > 15).name`. + +The JSON Path has 2 modes: lax (default) and strict. The first one allows to flex path and JSON content matching. +Missing fields and incorrect array ranges are ignored. The strict one produces an error. The full rules: +1) Invalid array range: lax - allowed, strict - error +2) Index of array range: lax - allowed, strict - error +3) Missing field: lax - allowed, strict - error +4) Error inside a JSON Path Filter: lax - hides (the), strict - throw error +5) Unwrapping. lax: for `$.name` with `[{"name":"John"},{"name":"Sam"}]`, the array will be unwrapped. A path will be consider as follow: `$[*].name`. Strict - error +6) Path unwrapping before methods (except `size()` and `type()`) in lax mode +7) Path unwrapping before filters in lax mode +8) Path unwrapping before unary `-` and `+` +9) Path unwrapping for arithmetic operands` +10) Wrapping. lax: for `$[*].name` with `{"name":"John"}`, the object will be wrapped. A path will be consider as follow: `$.name`. Strict - error + +The JSON PATH is used in JSON_VALUE, JSON_QUERY, JSON_EXISTS and JSON_TABLE +The full syntax: + +```sql + ::= + + + ::= lax | strict + + ::= + '$' [] + | '@' [] + | + | [] + | '(' ')' + + ::= '$' + + ::= + [] + | + | + | + + ::= [ ...] + + ::= + + | + | + | + | + + ::= + '.' + | '.' + + ::= + '.' '*' + + ::= + '[' ']' + + ::= + [ { }... ] + + ::= + + | 'last' + | 'last' - + | 'last' + + | to + | '*' + | + + + ::= + '+' + | '-' + + ::= + '+' + | '-' + | '*' + | '/' + | '%' + + + ::= + '?' '(' ')' + + ::= + type '(' ')' + | size '(' ')' + | double '(' ')' + | ceiling '(' ')' + | floor '(' ')' + | abs '(' ')' + | datetime '(' [ ] ')' + | keyvalue '(' ')' [] + | bigint '(' ')' + | boolean '(' ')' + | date '(' ')' + | integer '(' ')' + | number '(' ')' + | string '(' ')' + | time '(' ')' + | time_tz '('')' + | timestamp '('')' + | timestamp_tz '('')' + + ::= + + + + ::= + + | + + ::= + + | '(' ')' + + ::= + exists '(' ')' + + + ::= + + | + | + | + | + + ::= + + + ::= + '==' + | '!=' + | '<>' + | '<' + | '>' + | '<=' + | '>=' + + ::= + like_regex [ flag ] + + ::= + [. ] + | + + ::= + + ::= + + + ::= + starts with + + ::= + [. ] + | + + ::= + '(' ')' is unknown + + ::= + + | '!' + + + ::= + + | '||' + + ::= + + | '&&' +``` + +### Nodes +JSON Path should be a compile time constant + +### Examples: +```sql +SELECT JSON_VALUE('[1,2,3]', '$[2]') FROM RDB$DATABASE; +SELECT JSON_QUERY('[1,2,3]', '$ ? (@ > 1)' WITH WRAPPER) FROM RDB$DATABASE; +SELECT JSON_QUERY('[1,2,3]', 'strict $[*] ? (@ > 1)' WITH WRAPPER) FROM RDB$DATABASE; +SELECT JSON_QUERY('[1,-2,3]', 'strict $[*].abs()' WITH WRAPPER) FROM RDB$DATABASE; + +SELECT JSON_QUERY('[{"name":"John", "score":10}, {"name":"Sam", "score":50}]', '$ ? (@.score > 15).name' WITH WRAPPER) FROM RDB$DATABASE; +SELECT JSON_QUERY('{"items":[{"name":"John"}, {"name":"Sam", "score":50}]}', '$.items ? (exists(@.score) && @.score < 15).name' WITH WRAPPER) FROM RDB$DATABASE; + +``` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 13059dc5ebe..8c7a993d774 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -260,6 +260,34 @@ project_group(parse "Boot/Custom build steps") set_source_files_properties(dsql/parse.cpp include/gen/parse.h PROPERTIES GENERATED TRUE) +set(jpath_parse_src + ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/JsonPathParser.y + ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/btyacc_json.ske +) +add_custom_command( + OUTPUT jpath_tab.h jpath_tab.c + DEPENDS + btyacc + ${jpath_parse_src} + COMMAND sed -n "/%type .*/p" ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/JsonPathParser.y > jtypes.y + COMMAND sed "s/%type .*//" ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/JsonPathParser.y > json_y.y + COMMAND btyacc -l -d -b jpath -S ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/btyacc_json.ske json_y.y + + COMMAND sed -i "s/#define \\([A-Z].*\\)/#define TOK_\\1/g" jpath_tab.h + COMMAND sed -i "s/#define TOK_YY\\(.*\\)/#define YY\\1/g" jpath_tab.h + + COMMAND ${CMAKE_COMMAND} -E copy_if_different jpath_tab.h include/gen/jparse.h + COMMAND ${CMAKE_COMMAND} -E copy_if_different jpath_tab.c dsql/JsonPathParser.cpp + COMMENT "Generating JsonPathParser.cpp, JsonPathParser.h" + VERBATIM +) +add_custom_target(parse_jpath + DEPENDS jpath_tab.h jpath_tab.c + SOURCES ${parse_src} jpath_tab.h jpath_tab.c +) +project_group(parse_jpath "Boot/Custom build steps") +set_source_files_properties(dsql/JsonPathParser.cpp include/gen/jparse.h PROPERTIES GENERATED TRUE) + ######################################## # BUILD_STEP UpdateCloopInterfaces ######################################## diff --git a/src/common/CvtFormat.cpp b/src/common/CvtFormat.cpp index 25e911b3881..bd355a0e7c5 100644 --- a/src/common/CvtFormat.cpp +++ b/src/common/CvtFormat.cpp @@ -1820,6 +1820,60 @@ namespace return timestampTZ; } + + constexpr bool patternContainsDate(const Format::Patterns patterns) + { + return patterns & (Format::Y + | Format::YY + | Format::YYY + | Format::YYYY + | Format::YEAR + | Format::RR + | Format::RRRR + | Format::MM + | Format::MON + | Format::MONTH + | Format::RM + | Format::DD + | Format::J); + } + + constexpr bool patternContainsTime(const Format::Patterns patterns) + { + return patterns & (Format::HH + | Format::HH12 + | Format::HH24 + | Format::MI + | Format::SS + | Format::SSSSS + | Format::FF1 + | Format::FF2 + | Format::FF3 + | Format::FF4 + | Format::AM + | Format::PM); + } + + constexpr bool patternContainsTimezone(const Format::Patterns patterns) + { + return patterns & (Format::TZH + | Format::TZM + | Format::TZR); + } + + constexpr Firebird::CvtStringContains::TypeFlags getPatternTypeFlags(const Format::Patterns patterns) + { + Firebird::CvtStringContains::TypeFlags typeFlags = Firebird::CvtStringContains::NONE; + + if (patternContainsDate(patterns)) + typeFlags |= Firebird::CvtStringContains::DATE; + if (patternContainsTime(patterns)) + typeFlags |= Firebird::CvtStringContains::TIME; + if (patternContainsTimezone(patterns)) + typeFlags |= Firebird::CvtStringContains::TIMEZONE; + + return typeFlags; + } } string CVT_format_datetime_to_string(const dsc* desc, const string& format, Callbacks* cb) @@ -1842,7 +1896,8 @@ string CVT_format_datetime_to_string(const dsc* desc, const string& format, Call } ISC_TIMESTAMP_TZ CVT_format_string_to_datetime(const dsc* desc, const Firebird::string& format, - const EXPECT_DATETIME expectedType, Firebird::Callbacks* cb) + const EXPECT_DATETIME expectedType, Firebird::Callbacks* cb, + CvtStringContains::TypeFlags* outFormat) { if (!DTYPE_IS_TEXT(desc->dsc_dtype)) cb->err(Arg::Gds(isc_invalid_data_type_for_date_format)); @@ -1878,5 +1933,8 @@ ISC_TIMESTAMP_TZ CVT_format_string_to_datetime(const dsc* desc, const Firebird:: validateTimeStamp(timestampTZ.utc_timestamp, cvtData.times, expectedType, desc, cb); timeStampToUtc(timestampTZ, cb->getSessionTimeZone(), expectedType, cb); + if (outFormat) + *outFormat = getPatternTypeFlags(formatPatterns); + return timestampTZ; } diff --git a/src/common/CvtFormat.h b/src/common/CvtFormat.h index 39e2ab1bfee..96dd89253e5 100644 --- a/src/common/CvtFormat.h +++ b/src/common/CvtFormat.h @@ -6,6 +6,7 @@ Firebird::string CVT_format_datetime_to_string(const dsc* desc, const Firebird::string& format, Firebird::Callbacks* cb); ISC_TIMESTAMP_TZ CVT_format_string_to_datetime(const dsc* desc, const Firebird::string& format, - const Firebird::EXPECT_DATETIME expectedType, Firebird::Callbacks* cb); + const Firebird::EXPECT_DATETIME expectedType, Firebird::Callbacks* cb, + Firebird::CvtStringContains::TypeFlags* outFormat = nullptr); #endif // COMMON_CVT_FORMAT_H diff --git a/src/common/TimeZoneUtil.cpp b/src/common/TimeZoneUtil.cpp index 6533976f855..ac2ef01ab8d 100644 --- a/src/common/TimeZoneUtil.cpp +++ b/src/common/TimeZoneUtil.cpp @@ -1055,6 +1055,36 @@ ISC_TIMESTAMP_TZ TimeZoneUtil::dateToTimeStampTz(const ISC_DATE& date, Callbacks return tsTz; } + +int TimeZoneUtil::compareTimeStamps(ISC_TIMESTAMP_TZ lhs, ISC_TIMESTAMP_TZ rhs) +{ + if (lhs.time_zone != rhs.time_zone) + { + localTimeStampToUtc(lhs); + localTimeStampToUtc(rhs); + } + + return compareUtcTimeStamps(lhs.utc_timestamp, rhs.utc_timestamp); +} + +int TimeZoneUtil::compareUtcTimeStamps(const ISC_TIMESTAMP lhs, const ISC_TIMESTAMP rhs) +{ + const NoThrowTimeStamp leftTimestamp(lhs); + const NoThrowTimeStamp rightTimestamp(rhs); + if (leftTimestamp > rightTimestamp) + { + return 1; + } + else if (leftTimestamp < rightTimestamp) + { + return -1; + } + else + { + return 0; + } +} + //------------------------------------- TimeZoneRuleIterator::TimeZoneRuleIterator(USHORT id, const ISC_TIMESTAMP_TZ& aFrom, const ISC_TIMESTAMP_TZ& aTo) diff --git a/src/common/TimeZoneUtil.h b/src/common/TimeZoneUtil.h index 6e054356329..bcabd78f5f2 100644 --- a/src/common/TimeZoneUtil.h +++ b/src/common/TimeZoneUtil.h @@ -141,6 +141,9 @@ class TimeZoneUtil static ISC_TIME_TZ timeStampToTimeTz(const ISC_TIMESTAMP& timeStamp, Callbacks* cb); static ISC_TIMESTAMP_TZ dateToTimeStampTz(const ISC_DATE& date, Callbacks* cb); + + static int compareTimeStamps(ISC_TIMESTAMP_TZ lhs, ISC_TIMESTAMP_TZ rhs); + static int compareUtcTimeStamps(const ISC_TIMESTAMP lhs, const ISC_TIMESTAMP rhs); }; class IcuCalendarWrapper diff --git a/src/common/classes/BlrReader.h b/src/common/classes/BlrReader.h index cc654220d28..40f5ba1d276 100644 --- a/src/common/classes/BlrReader.h +++ b/src/common/classes/BlrReader.h @@ -201,10 +201,8 @@ class BlrReader return word; } - void getString(string& s) + inline void getString(string& s, const unsigned len) { - const unsigned len = getByte(); - if (pos + len >= end) (Arg::Gds(isc_invalid_blr) << Arg::Num(getOffset())).raise(); @@ -213,6 +211,20 @@ class BlrReader seekForward(len); } + + inline void getString(string& s) + { + const unsigned len = getByte(); + getString(s, len); + } + + inline void getVerbString(string& s) + { + getByte(); // verb + const unsigned len = getWord(); + getString(s, len); + } + template void getMetaName(STR& name) { diff --git a/src/common/classes/NoThrowTimeStamp.h b/src/common/classes/NoThrowTimeStamp.h index 201e9ebaaa6..072fa34c50e 100644 --- a/src/common/classes/NoThrowTimeStamp.h +++ b/src/common/classes/NoThrowTimeStamp.h @@ -191,6 +191,28 @@ class NoThrowTimeStamp return ts; } + inline bool operator>(const NoThrowTimeStamp& rhs) const + { + const auto rhsValue = rhs.value(); + + if (mValue.timestamp_date > rhsValue.timestamp_date) + return true; + if (mValue.timestamp_date < rhsValue.timestamp_date) + return false; + return mValue.timestamp_time > rhsValue.timestamp_time; + }; + + inline bool operator<(const NoThrowTimeStamp& rhs) const + { + const auto rhsValue = rhs.value(); + + if (mValue.timestamp_date < rhsValue.timestamp_date) + return true; + if (mValue.timestamp_date > rhsValue.timestamp_date) + return false; + return mValue.timestamp_time < rhsValue.timestamp_time; + }; + private: ISC_TIMESTAMP mValue; diff --git a/src/common/classes/fb_string.h b/src/common/classes/fb_string.h index 657da3fe054..6366a1a6c13 100644 --- a/src/common/classes/fb_string.h +++ b/src/common/classes/fb_string.h @@ -793,6 +793,10 @@ namespace Firebird { return add(&c, 1); } + StringType& operator=(const std::string_view v) + { + return assign(v.data(), static_cast(v.length())); + } StringType& operator=(StringType&& rhs) { // baseMove do not clear the buffer so do it in this method @@ -902,6 +906,7 @@ namespace Firebird bool operator>=(const char_type* str) const {return compare(str) >= 0;} bool operator> (const char_type* str) const {return compare(str) > 0;} bool operator!=(const char_type* str) const {return different(str);} + operator std::string_view() const { return std::string_view(stringBuffer, stringLength); } bool getWord(StringType& from, const char* sep) { diff --git a/src/common/cvt.cpp b/src/common/cvt.cpp index da9b6c40d6a..a35256bcd09 100644 --- a/src/common/cvt.cpp +++ b/src/common/cvt.cpp @@ -675,7 +675,8 @@ static void integer_to_text(const dsc* from, dsc* to, Callbacks* cb) void CVT_string_to_datetime(const dsc* desc, - ISC_TIMESTAMP_TZ* date, bool* timezone_present, + ISC_TIMESTAMP_TZ* date, + CvtStringContains::TypeFlags* outFormatFlags, const EXPECT_DATETIME expect_type, bool allow_special, Callbacks* cb) { /************************************** @@ -757,8 +758,8 @@ void CVT_string_to_datetime(const dsc* desc, memset(components, 0, sizeof(components)); memset(description, 0, sizeof(description)); - if (timezone_present) - *timezone_present = false; + if (outFormatFlags) + *outFormatFlags = CvtStringContains::NONE; // Parse components // The 7 components are Year, Month, Day, Hours, Minutes, Seconds, Thou @@ -948,6 +949,9 @@ void CVT_string_to_datetime(const dsc* desc, continue; else if (i >= 3 && i <= 5) { + if (outFormatFlags) + *outFormatFlags |= CvtStringContains::TIME; + if (*p == ':') { p++; @@ -998,8 +1002,8 @@ void CVT_string_to_datetime(const dsc* desc, { zone = TimeZoneUtil::parse(p, end - p); - if (timezone_present) - *timezone_present = true; + if (outFormatFlags) + *outFormatFlags |= CvtStringContains::TIMEZONE; } } else @@ -1076,6 +1080,9 @@ void CVT_string_to_datetime(const dsc* desc, times.tm_mon = components[position_month]; times.tm_mday = components[position_day]; + if (outFormatFlags) + *outFormatFlags |= CvtStringContains::DATE; + // Fetch current date/time tm times2; Firebird::TimeStamp::getCurrentTimeStamp().decode(×2); diff --git a/src/common/cvt.h b/src/common/cvt.h index 984e6f151d9..1664794e52a 100644 --- a/src/common/cvt.h +++ b/src/common/cvt.h @@ -81,6 +81,16 @@ enum EXPECT_DATETIME expect_sql_time_tz }; +namespace CvtStringContains +{ + typedef UCHAR TypeFlags; + + constexpr TypeFlags NONE = 0; + constexpr TypeFlags TIME = 1 << 0; + constexpr TypeFlags DATE = 1 << 1; + constexpr TypeFlags TIMEZONE = 1 << 2; +} + class Int128; } // namespace Firebird @@ -103,7 +113,7 @@ USHORT CVT_get_string_ptr(const dsc*, TTypeId*, UCHAR**, vary*, USHORT, Firebird USHORT CVT_get_string_ptr_common(const dsc*, TTypeId*, UCHAR**, vary*, USHORT, Firebird::DecimalStatus, Firebird::Callbacks*); SINT64 CVT_get_int64(const dsc*, SSHORT, Firebird::DecimalStatus, ErrorFunction); SQUAD CVT_get_quad(const dsc*, SSHORT, Firebird::DecimalStatus, ErrorFunction); -void CVT_string_to_datetime(const dsc*, ISC_TIMESTAMP_TZ*, bool*, const Firebird::EXPECT_DATETIME, +void CVT_string_to_datetime(const dsc*, ISC_TIMESTAMP_TZ*, Firebird::CvtStringContains::TypeFlags*, const Firebird::EXPECT_DATETIME, bool, Firebird::Callbacks*); const UCHAR* CVT_get_bytes(const dsc*, unsigned&); diff --git a/src/common/tests/CvtTestUtils.h b/src/common/tests/CvtTestUtils.h index b7917242419..fa2b57a9853 100644 --- a/src/common/tests/CvtTestUtils.h +++ b/src/common/tests/CvtTestUtils.h @@ -29,7 +29,7 @@ static constexpr int sign(T value) return (value >= T(0)) ? 1 : -1; } -static ISC_DATE mockGetLocalDate(int year = 2023) +inline ISC_DATE mockGetLocalDate(int year = 2023) { struct tm time; memset(&time, 0, sizeof(time)); @@ -41,7 +41,7 @@ static ISC_DATE mockGetLocalDate(int year = 2023) // Pass 0 to year, month and day to use CurrentTimeStamp for them -static struct tm initTMStruct(int year, int month, int day) +inline struct tm initTMStruct(int year, int month, int day) { struct tm currentTime; NoThrowTimeStamp::decode_date(mockGetLocalDate(), ¤tTime); @@ -58,19 +58,19 @@ static struct tm initTMStruct(int year, int month, int day) } // Pass 0 to year, month and day to use CurrentTimeStamp for them -static ISC_DATE createDate(int year, int month, int day) +inline ISC_DATE createDate(int year, int month, int day) { struct tm times = initTMStruct(year, month, day); return NoThrowTimeStamp::encode_date(×); } -static ISC_TIME createTime(int hours, int minutes, int seconds, int fractions = 0) +inline ISC_TIME createTime(int hours, int minutes, int seconds, int fractions = 0) { return NoThrowTimeStamp::encode_time(hours, minutes, seconds, fractions); } // Pass 0 to year, month and day to use CurrentTimeStamp for them -static ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, int minutes, int seconds, int fractions = 0) +inline ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, int minutes, int seconds, int fractions = 0) { struct tm times = initTMStruct(year, month, day); times.tm_hour = hours; @@ -81,7 +81,7 @@ static ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, in } // Pass 0 to year, month and day to use CurrentTimeStamp for them -static ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hours, int minutes, int seconds, +inline ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0) { ISC_TIMESTAMP_TZ timestampTZ; @@ -94,7 +94,7 @@ static ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hour return timestampTZ; } -static ISC_TIME_TZ createTimeTZ(int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0) +inline ISC_TIME_TZ createTimeTZ(int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0) { // Day is 2 because we need to handle 00:00 with negative timezone offset, and anyway date is not used in TIME WITH TIME ZONE ISC_TIMESTAMP_TZ timestampTz = createTimeStampTZ(1, 1, 2, hours, minutes, seconds, offsetInMinutes, fractions); diff --git a/src/dsql/make.cpp b/src/dsql/make.cpp index e30e24359ba..221e886f505 100644 --- a/src/dsql/make.cpp +++ b/src/dsql/make.cpp @@ -262,11 +262,15 @@ ValueExprNode* MAKE_constant(const char* str, dsql_constant_type numeric_flag, S // Now invoke the string_to_date/time/timestamp routines ISC_TIMESTAMP_TZ ts; - bool tz; - CVT_string_to_datetime(&tmp, &ts, &tz, expect1, false, &EngineCallbacks::instance); + CvtStringContains::TypeFlags flags; + CVT_string_to_datetime(&tmp, &ts, &flags, expect1, false, &EngineCallbacks::instance); + bool tz = flags & CvtStringContains::TIMEZONE; if (!tz && expect1 != expect2) - CVT_string_to_datetime(&tmp, &ts, &tz, expect2, false, &EngineCallbacks::instance); + { + CVT_string_to_datetime(&tmp, &ts, &flags, expect2, false, &EngineCallbacks::instance); + tz = flags & CvtStringContains::TIMEZONE; + } switch (numeric_flag) { diff --git a/src/jrd/json/JsonConsts.h b/src/jrd/json/JsonConsts.h new file mode 100644 index 00000000000..6b9ad9d6207 --- /dev/null +++ b/src/jrd/json/JsonConsts.h @@ -0,0 +1,347 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonConsts.h + * DESCRIPTION: JSON constants. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2023 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_CONSTS_H +#define JSON_CONSTS_H + +#include "firebird.h" + +#include "../common/classes/fb_string.h" +#include "../common/StatusArg.h" + +#include "../jrd/intl.h" +#include "../common/TextType.h" + + +static constexpr TTypeId JSON_TTYPE = ttype_utf8; +static constexpr USHORT JSON_TTYPE_BYTES_BET_CHAR = 4; +static constexpr USHORT JSON_DEFAULT_SIZE = 6000 * JSON_TTYPE_BYTES_BET_CHAR; + +using TextPos = FB_UINT64; +using TextLength = FB_UINT64; + +static constexpr TextPos INVALID_TEXT_POS = MAX_UINT64; +static constexpr TextLength JSON_MAX_REPORT_SIZE = 50; + + +// Tokens +namespace FBJSON +{ + // usings + + using SmallString = Firebird::string; + using JsonToken = std::string_view; + using JsonArrayIndex = USHORT; + + enum PlanType : UCHAR // Ordered by priority + { + DEFAULT = 0, + INNER, // one to one from nested + OUTER, // one to many with nested + UNION, // 2 columns: print full one, then print full the second:, + CROSS // 2 columns: print columns at the same moment: + }; + + enum class CompareResult : int + { + EQUALS = 0, + LESS = -1, + MORE = 1 + }; + + + // Basically the inversion + constexpr CompareResult operator*(CompareResult lhs, const bool invert) + { + if (!invert) + return lhs; + + return static_cast(-static_cast(lhs)); + } + + inline CompareResult castCompareResult(const int r) + { + if (r < 0) + return CompareResult::LESS; + else if (r > 0) + return CompareResult::MORE; + else + return CompareResult::EQUALS; + } + + namespace Tokens + { + namespace TypeName + { + constexpr JsonToken EMPTY = "empty"; + constexpr JsonToken STRING = "string"; + constexpr JsonToken NUMBER = "number"; + constexpr JsonToken BOOLEAN = "boolean"; + constexpr JsonToken JNULL = "null"; + constexpr JsonToken ARRAY = "array"; + constexpr JsonToken OBJECT = "object"; + constexpr JsonToken DATETIME = "datetime"; + constexpr JsonToken UNKNOWN = "unknown"; + constexpr JsonToken TIME_WITH_TZ = "time with time zone"; + constexpr JsonToken TIME_WITHOUT_TZ = "time without time zone"; + constexpr JsonToken TIMESTAMT_WITH_TZ = "timestamp with time zone"; + constexpr JsonToken TIMESTAMT_WITHOUT_TZ = "timestamp without time zone"; + constexpr JsonToken DATE = "date"; + } + + const JsonToken ANY_FIELD("*"); + } +} + + +// using JsonIscStatus = ISC_STATUS; +// using JsonStatusVector = Firebird::Arg::StatusVector; +// using JsonStatusMsg = Firebird::Arg::Gds; +// using JsonStatusMsgIntArg = Firebird::Arg::Num; +// using JsonStatusMsgStrArg = Firebird::Arg::Str; + +// Temporary classes to avoid constantly moving codes msg/jrd.h; +// It will be replaced by the real jrd codes +struct JsonStatusMsgWrapper; +using JsonIscStatus = const char*; +using JsonStatusVector = JsonStatusMsgWrapper; +using JsonStatusMsg = JsonStatusMsgWrapper; +using JsonStatusMsgIntArg = JsonStatusMsgWrapper; +using JsonStatusMsgStrArg = JsonStatusMsgWrapper; + +struct JsonStatusMsgWrapper +{ + Firebird::ObjectsArray impl; + mutable Firebird::Arg::StatusVector status; + + JsonStatusMsgWrapper() noexcept : impl(*getDefaultMemoryPool()), status() + { } + + JsonStatusMsgWrapper(SINT64 value) : impl(*getDefaultMemoryPool()), status() + { + Firebird::string str; + str.printf("%" SQUADFORMAT, value); + impl.push(str); + } + + + JsonStatusMsgWrapper(const Firebird::string& se) : impl(*getDefaultMemoryPool()), status() + { + impl.push(se); + } + + JsonStatusMsgWrapper(const std::string_view sv) : impl(*getDefaultMemoryPool()), status() + { + impl.push(Firebird::string(sv.data(), static_cast(sv.length()))); + } + + JsonStatusMsgWrapper(const char* rawStr) : impl(*getDefaultMemoryPool()), status() + { + Firebird::string str(rawStr); + impl.push(str); + } + + JsonStatusMsgWrapper(const char* rawStr, ULONG length) : impl(*getDefaultMemoryPool()), status() + { + Firebird::string str(rawStr, length); + impl.push(str); + } + + // StatusVector case - append multiple args + JsonStatusMsgWrapper& operator<<(const JsonStatusMsgWrapper& arg) + { + for (FB_SIZE_T i = 0; i < arg.impl.getCount(); i++) + { + impl.push(arg.impl[i]); + } + + return *this; + } + + bool isDirty() const + { + return impl.hasData(); + } + + const ISC_STATUS* value() const + { + status.assign(Firebird::Arg::Gds(isc_random) << Firebird::Arg::Str(getMessage())); + return status.value(); + } + + Firebird::string getMessage() const + { + FB_SIZE_T msgNum = 0; + Firebird::string outMsg; + // Append non args to the end of the message + for (; msgNum < impl.getCount(); msgNum++) + { + FB_SIZE_T maxUsedMsg = 0; + const Firebird::string& mainMsg = impl[msgNum]; + for (FB_SIZE_T i = 0; i < mainMsg.length(); i++) + { + const char curChar = mainMsg[i]; + if (i < mainMsg.length() - 1 && curChar == '@' && + (mainMsg[i + 1] >= '0' && mainMsg[i + 1] <= '9')) + { + const FB_SIZE_T digit = mainMsg[i + 1] - '0'; + if (msgNum + digit >= impl.getCount()) + { + fb_assert(false); + return ""; + } + else + { + maxUsedMsg = maxUsedMsg > digit ? maxUsedMsg : digit; + outMsg += impl[msgNum + digit]; + i++; + } + } + else + outMsg += curChar; + } + + msgNum += maxUsedMsg; // Skip args + } + + return outMsg; + } +}; + +#define isc_jlexer_invalid_syntax "Invalid input syntax" +#define isc_jlexer_time_precision_syntax "Time precision should be positive" +#define isc_jlexer_end "Unexpected end of command at position @1" +#define isc_jlexer_end_with_line "Unexpected end of command at position @1 (line @2)" +#define isc_jlexer_passing_not_defined "Passing clause has not been set" +#define isc_jlexer_passing_var_not_defined "Variable '@1' has not been passed. Note that the passing name is case-sensitive" +#define isc_jpath_common "JSON Path: " +#define isc_jpath_invalid_token "Invalid input syntax\nToken \"@1\" is invalid" +#define isc_jpath_problem_place "\n-At position @1\n->@2" +#define isc_jpath_problem_place_with_line "\n-At position @1 (line @2)\n->@3" +#define isc_jpath_multiple_roots "Usage of multiple root accessors is not allowed for current JSON function" +#define isc_jpath_math_is_forbidden "Using methods or arithmetics is not allown in for current function" +#define isc_jpath_illegal_item_variable "Using @ variable is allowed only in a filter expression" +#define isc_jpath_missing_root "The root $ expression is missing" +#define isc_jpath_invalid_range "Range value should be an INTEGRAL value in range [@1 to @2]" +#define isc_jstrict_common "strict mode: " +#define isc_jstrict_out_of_range "Range [@1 to @2] is out of array bounds (0..@3)" +#define isc_jstrict_invalid_range "Range [@1 to @2] is invalid" +#define isc_jstrict_filed_name_mismatched "Object filed \"@1\" has not been found" +#define isc_jstrict_path_mismatched "Json Path does not match JSON structure" +#define isc_jstrict_non_array_size "Cannot calculate size for a non-array token" +#define isc_jfilter_common "JSON Path Filter: " +#define isc_jword_string "string" +#define isc_jword_left "left" +#define isc_jword_right "right" +#define isc_jword_unary "unary" +#define isc_jword_unary_minus "unary minus" +#define isc_jword_unary_plus "unary plus" +#define isc_jword_bool_or_unknown "Boolean or Unknown" +#define isc_jparser_common "JSON parser: invalid input syntax for JSON.\n" +#define isc_jparser_input "JSON data: @1" +#define isc_jparser_end_unexpectedly "Expected @1, but encountered the end of the input\n" +#define isc_jparser_expected_token "Expected @1, but found \"@2\".\n" +#define isc_jparser_string "string" +#define isc_jparser_object_end "'}'" +#define isc_jparser_object_colon "':'" +#define isc_jparser_array_end "']'" +#define isc_jparser_value "value" +#define isc_jparser_end "end of the input" +#define isc_jparser_unescaped_character "Character with value 0x@1 must be escaped\n" +#define isc_jparser_un_incorrect_code "The unicode sequence '\\u@1' does not consist of 4 hexadecimal digits\n" +#define isc_jparser_un_double_high_surrogate "Unicode lead surrogate '\\u@1' must not follow a lead surrogate (U+D800 to U+DBFF)\n" +#define isc_jparser_un_missing_high_surrogate "The unicode tail surrogate '\\u@1' must follow a lead surrogate (U+D800 to U+DBFF)\n" +#define isc_jparser_un_conv_error "The Unicode sequence '\\u0000' cannot be converted to text\n" +#define isc_jparser_invalid_sequence "Escape sequence '\\@1' is invalid\n" +#define isc_jparser_invalid_token "Token \"@1\" is invalid\n" +#define isc_jparser_invalid_number_parse "The value \"@1\" cannot be converted to a number\n" +#define isc_jparser_empty_input "Empty value cannot be converted to a number\n" +#define isc_jparser_invalid_quoted_string "Invalid quoted string: the closing quote is missing\n" +#define isc_jparser_number_overflow "Data overflow for number @1\n" +#define isc_jdyn_common "JSON processor\n" +#define isc_jdyn_get_double_invalid_type_error "Number expected, but invalid type encountered" +#define isc_jdyn_get_double_string_type_error "Number expected, but string '@1' encountered" +#define isc_jdyn_internal_error "internal error (bug)" +#define isc_jdyn_to_double_error "The statement '@1' cannot be converted to a number" +#define isc_jdyn_invalid_expression_before_is_unknown "Invalid expression before 'is unknown' predicate" +#define isc_jdyn_operand_error "@1 operand of operator @2 must be @3" +#define isc_jdyn_number_operand_error "@1 operand of operator @2 must be a number" // , but a @3 was provided +#define isc_jdyn_both_not_the_type "Both operations of component @1 must be @2" +#define isc_jdyn_missing_bool_op "A value without a condition cannot produce a boolean result" +#define isc_jdyn_missing_value_for_op "Missing value before or after an operand" +#define isc_jpath_operations_limit "The operations limit has been exceeded (@1, max allowed @2)" +#define isc_jdyn_json_wrong_type "The value in the JSON representation is expected as a string" +#define isc_jdyn_invalid_statement "Invalid statement" +#define isc_jdyn_returns_multiple_values "@1 path expression returns more than one value" +#define isc_jdyn_returns_non_scalar "@1 path expression returned a non-scalar value" +#define isc_jdyn_missing_wrapper_scalar "@1 should be used with the ARRAY WRAPPER clause because the result has a scalar value" +#define isc_jdyn_missing_wrapper_several "@1 should be used with the ARRAY WRAPPER clause because the result consists of several values" +#define isc_jdyn_incorrect_filter_result "The result of the filter calculation is not a boolean or unknown value" +#define isc_jdyn_type_unknown_token "type(): An unknown token has been passed" +#define isc_jdyn_regexflags_not_string "like_regex: The flags clause type is not a string" +#define isc_jdyn_unknown_regexflag "like_regex: Unknown flag '@1'" +#define isc_jdyn_flags_illegal_usage "like_regex: Illegal usage of a regex flags clause" +#define isc_jdyn_token_parsing_error "@1: Error while parsing the input string\n" +#define isc_jdyn_to_number_error "@1: A non-string or non-number value has been passed" +#define isc_jdyn_path_empty_result "Cannot operate with empty value/sequence in the current expression" +#define isc_jdyn_function_empty_result "@1 returned an empty value" +#define isc_jdyn_key_is_null "A JSON key cannot be NULL" +#define isc_jdyn_invalid_key_type "Invalid key: Only a character type is allowed" +#define isc_jdyn_nonunique_keys "There is a non-unique key with the name '@1' at indexes @2 and @3" +#define isc_jdyn_missing_passing_id "Passing name \"@1\" is not defined" +#define isc_jmodify_insert_missing_index "Modify error: INSERT mode requires an index range, not an array itself; Use APPEND mode in this case" +#define isc_jmodify_insert_incompatible "Modify error: INSERT mode is incompatible with the matched JSON token" +#define isc_jmodify_insert_duplicate "Modify error: attempt to INSERT duplicate key. Use UPDATE mode" +#define isc_jmodify_mode_vs_object_incompatible "Modify error: Current mode is incompatible with an object" +#define isc_jmodify_mode_vs_scalar_incompatible "Modify error: Current mode is incompatible with a scalar" +#define isc_jmodify_missing_item "Item to modify has not been found" +#define isc_jdyn_not_json "The JSON is not in a proper format" +#define isc_jdyn_duplicate_fields "The JSON contains duplicate fields" +#define isc_jdyn_keyvalue_with_scalar "scalar is incomputable with keyvalue()" +#define isc_jdyn_keyvalue_with_array "array is incomputable with keyvalue()" +#define isc_jdyn_keyvalue_with_null "null is incomputable with keyvalue()" +#define isc_jdyn_keyvalue_with_invalid_type "Invalid input type for keyvalue()" +#define isc_jdyn_size_invalid_type "Invalid input type for size()" +#define isc_jdyn_datetime_invalid_type "Invalid input type for datetime()" +#define isc_jdyn_number_limit "Input value is out of JSON number limit" +#define isc_jdyn_keyvalue_returned_sequence "keyvalue() returned several values an empty sequence" +#define isc_jdyn_incorrect_path_if_result "The result of the path filter calculation is not a Boolean or Unknown value" +#define isc_jdyn_datetime_incompatible "Current type is incompatible with the datetime() method" +#define isc_jdyn_boolean_cast_type "@1: Invalid input type for boolean conversion" +#define isc_jdyn_boolean_cast_value "@1: The input text cannot be parsed as boolean" +#define isc_jdyn_json_to_string_prefix "@1: " +#define isc_jdyn_json_to_string_cast_error "Cannot convert JSON Object or JSON Array to string" +#define isc_jdyn_to_int_error "@1: Invalid input type for number conversion\n" +#define isc_jout_enum_with_multiple_columns "A query expression argument in the JSON_ARRAY function must return exactly one column" +#define isc_jdsc_invalid_subtype "Invalid or not specified JSON subtype" +#define isc_jdsc_invalid_charset "JSON should be in UTF8 representation" +#define isc_jdsc_concatenate_unsupported "Concatenation of JSON type is unsupported" +#define isc_jdsc_non_scalar "The input value is not a scalar" +#define isc_jdsc_cannot_convert_json_to_dsc "Cannot convert JSON value to a scalar" +#define isc_jdsc_too_big "The JSON string is too big (the length is @1)" + + +#endif // JSON_CONSTS_H diff --git a/src/jrd/json/JsonRuntime.cpp b/src/jrd/json/JsonRuntime.cpp new file mode 100644 index 00000000000..e10b88051e0 --- /dev/null +++ b/src/jrd/json/JsonRuntime.cpp @@ -0,0 +1,298 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonRuntime.cpp + * DESCRIPTION: Json Calculation logic. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2022 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "JsonRuntime.h" + +#include "JsonConsts.h" + +#include "path/JsonPath.h" +#include "classes/JsonScalar.h" +#include "classes/JsonTypes.h" + +#include + + +using namespace FBJSON; + +// **** +// Counting sort helpers + +constexpr int getOperationIndex(const JsonExprOperation op) +{ + return static_cast(op) - JsonExprNode::OFFSET_START; +} + +constexpr UCHAR moveOffsetByOperation(JsonExprNode::OffsetCounter& m_offsets, const JsonExprOperation op) +{ + return m_offsets[getOperationIndex(op)]++; +} +// **** + +constexpr int isUnaryOperation(const JsonExprOperation op) +{ + return JsonExprOperation::REGEX_FLAGS < op && op < JsonExprOperation::MULTIPLICATION; +} + + +JsonExprNode::JsonExprNode(MemoryPool& pool, const Type m_type, const UCHAR flag) + : Firebird::PermanentStorage(pool), + m_data(), + m_type(m_type), + m_flags(flag) +{ + switch (m_type) + { + case SCALAR_NODE: + m_data.scalarBlock = FB_NEW_POOL(pool) ScalarBlock(pool); + break; + case VARIABLE_NODE: + m_data.varBlock = FB_NEW_POOL(pool) VariableBlock(pool); + break; + case CALCULATION_NODE: + m_data.calculationBlock = FB_NEW_POOL(pool) CalculationBlock(pool); + break; + case METHOD_NODE: + m_data.methodBlock = FB_NEW_POOL(pool) MethodBlock(pool); + break; + case FILTER_NODE: + m_data.filterBlock = FB_NEW_POOL(pool) FilterBlock(pool); + break; + case COMPOUND_NODE: + m_data.childrenBlock = FB_NEW_POOL(pool) ChildrenNodes(pool); + break; + default: + fb_assert(false); + } +} + +// Variable node +JsonExprNode::JsonExprNode(MemoryPool& pool, PathVariable* var) : + JsonExprNode(pool, JsonExprNode::VARIABLE_NODE) +{ + fb_assert(var); + m_data.varBlock = FB_NEW_POOL(pool) VariableBlock(pool); + m_data.varBlock->variable.reset(var); +} + +// Method node +JsonExprNode::JsonExprNode(MemoryPool& pool, const PathMethod method): + JsonExprNode(pool, JsonExprNode::METHOD_NODE) +{ + m_data.methodBlock = FB_NEW_POOL(pool) MethodBlock(pool); + + m_data.methodBlock->method = method; + m_data.methodBlock->children.add(nullptr); +} + + +JsonExprNode::~JsonExprNode() +{ + if (canAccessChildren()) + { + auto& block = *m_data.childrenBlock; + for (FB_SIZE_T i = 0; i < block.children.getCount(); i++) + delete block.children[i]; + + block.children.clear(); + } + + switch (m_type) + { + case SCALAR_NODE: + delete m_data.scalarBlock; + break; + case VARIABLE_NODE: + delete m_data.varBlock; + break; + case CALCULATION_NODE: + delete m_data.calculationBlock; + break; + case METHOD_NODE: + delete m_data.methodBlock; + break; + case FILTER_NODE: + delete m_data.filterBlock; + break; + case COMPOUND_NODE: + delete m_data.filterBlock; + break; + default: + fb_assert(false); + } + + m_data = {}; +} + +JsonExprNode* JsonExprNode::addChild(const Type childType, const Flags flags) +{ + fb_assert(childType != CALCULATION_NODE || (flags & FLAG_SOLID) != 0); // It is useless to add CALCULATION_NODE with no SOLID flag + JsonExprNode* currentLevel = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), childType, flags); + return addChild(currentLevel) ? currentLevel : nullptr; +} + +bool JsonExprNode::addChild(JsonExprNode* node) +{ + fb_assert(node); + + fb_assert((m_type == CALCULATION_NODE || m_type == COMPOUND_NODE) // Any count of the m_children) + || (m_type == METHOD_NODE && m_data.childrenBlock->children.getCount() < 3) // 3 is max(primary, argument, afterpath) + || (m_type == FILTER_NODE && m_data.childrenBlock->children.getCount() < 3) // 3 is max + || (m_type == VARIABLE_NODE && m_data.childrenBlock->children.getCount() < 1)); // 1 is max + + // A statement is stored in a binary tree + // For example: + // '1 + 2 * 3' transforms to 'calculation_node(value_node, calculation_node(value_node, value_node))' + // It To avoid extra memory usage and simplify the structure we keep only CALCULATION_NODE with brackets + if (m_type == CALCULATION_NODE && node->m_type == CALCULATION_NODE && (node->m_flags & FLAG_SOLID) == 0) + { + for (FB_SIZE_T i = 0; i < node->getChildrenCount(); ++i) + { + auto* child = node->m_data.calculationBlock->children[i]; + m_data.childrenBlock->children.add(child); + } + + auto& rhsOperations = node->m_data.calculationBlock->operations; + for (FB_SIZE_T i = 0; i < rhsOperations.getCount(); ++i) + addOperation(rhsOperations[i]); + + // Explicit clear to avoid calling `delete` + node->m_data.calculationBlock->children.clear(); + delete node; + return false; + } + else + { + m_data.childrenBlock->children.add(node); + + return true; + } +} + +void JsonExprNode::addOperation(const JsonExprOperation operation) +{ + fb_assert(m_type == CALCULATION_NODE); + + m_data.calculationBlock->operations.add(operation); + + if (m_data.calculationBlock->operations.getCount() > OPERATIONS_LIMIT) + { + json_syntax_exception::raise(JsonStatusMsg(isc_jpath_operations_limit)<< + JsonStatusMsgIntArg(OPERATIONS_LIMIT + 1) << JsonStatusMsgIntArg(OPERATIONS_LIMIT)); + } + + moveOffsetByOperation(m_data.calculationBlock->offsets, operation); +} + +void JsonExprNode::makeHeadNode(const bool unwrap) +{ + fb_assert(canAccessChildren()); + + // Variable + PathVariable* var = FB_NEW_POOL(getPool()) PathVariable(getPool()); + var->type = PathVariable::Type::HEAD; + var->path = FB_NEW_POOL(getPool()) JsonPath(getPool()); + + // Node + JsonExprNode* headNode = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), var); + + // Unwrap + if (unwrap) + { + // Only for argument + headNode->unwrap(); + } + + // Add head + auto& children = m_data.childrenBlock->children; + if (children.getCount() > 0 && children[0] == nullptr) + { + fb_assert(testType(METHOD_NODE)); + setHead(headNode); + } + else + addChild(headNode); +} + + +JsonExprNode* JsonExprNode::finish() +{ + fb_assert(m_type == CALCULATION_NODE); + if (canBeOmitted()) // Possible useless + { + auto& children = m_data.calculationBlock->children; + auto* node = children.front(); + children.clear(); + delete this; + + return node; + } + + // Counting sort + auto& offsets = m_data.calculationBlock->offsets; + int lastValue = offsets[0]; + for (USHORT i = 1; i < offsets.size(); ++i) + { + const UCHAR temp = offsets[i]; + offsets[i] += offsets[i - 1]; + offsets[i - 1] -= lastValue; + lastValue = temp; + } + fb_assert(offsets.back() == m_data.calculationBlock->operations.getCount()); + offsets.back() -= lastValue; + + return this; +} + +const PathVariable* JsonExprNode::getVariable() const +{ + if (m_type == VARIABLE_NODE) + return m_data.varBlock->variable.get(); + else + return nullptr; +} + +void JsonExprNode::unwrap() +{ + if (hasTail()) + { + // Recursive call! + m_data.calculationBlock->children.back()->unwrap(); + return; + } + + // To unwrap a path, we need a variable node with data + if (!isVariable()) + return; + + auto* variable = m_data.varBlock->variable.get(); + if (variable == nullptr) + return; + + if (variable->path == nullptr) + variable->path.reset(FB_NEW_POOL(getPool()) JsonPath(getPool())); + + variable->path->unwrap(); +} diff --git a/src/jrd/json/JsonRuntime.h b/src/jrd/json/JsonRuntime.h new file mode 100644 index 00000000000..31a8cfddf37 --- /dev/null +++ b/src/jrd/json/JsonRuntime.h @@ -0,0 +1,528 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonRuntime.h + * DESCRIPTION: JSON calculation logic. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2022 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_RUNTIME_H +#define JSON_RUNTIME_H + +#include "firebird.h" +#include "JsonConsts.h" +#include "classes/JsonScalar.h" +#include "classes/JsonTypes.h" + +namespace FBJSON { + +struct ContextVariables; + +// operations order by its priority +enum class JsonExprOperation : UCHAR +{ + // VALUE, + NONE = 0, + + + // Functions + STARTS_WITH, + LIKE_REGEX, + REGEX_FLAGS, + + // Unary operations + IS_UNKNOWN, + EXISTS, + + // Unary + UNARY_PLUS, + UNARY_MINUS, + + // Logical negation + NOT, + + // Arithmetic operations + MULTIPLICATION, + DIVISION, + MODULO, + + ADDITION, + SUBTRACTION, + + // Logical operations + MORE, + MORE_OE, + LESS, + LESS_OE, + EQUALS, + NOT_EQUALS, + + // Boolean operations + AND, + OR, + + // Keep it last + LAST +}; + +class PassingList; +struct PathVariable; + + +// * * * * +// A main class to represent and execute complex expressions with arithmetics, comparators and methods +// All logic is set within a single class. It is not worth of splitting it into several classes +// because it only leads to overcomplicated a pretty simple and limited functional + +// Some node has a special storage strategy for children nodes: +// is a primary value (usually scalar or a variable) +// are used for the current node +// is a next execution node + +// * * * * +class JsonExprNode : public Firebird::PermanentStorage +{ +public: + static constexpr USHORT OFFSET_START = static_cast(JsonExprOperation::NONE) + 1; + static constexpr USHORT OFFSET_SIZE = static_cast(JsonExprOperation::LAST) - OFFSET_START; + static constexpr USHORT OPERATIONS_LIMIT = 128; + + using OffsetCounter = std::array; + + enum Type : UCHAR + { + SCALAR_NODE, // Just a value + VARIABLE_NODE, // $. @. $passingVariable + CALCULATION_NODE, // An expression + METHOD_NODE, // Method with args + FILTER_NODE, // Filter: $ ? (@ > 2) + COMPOUND_NODE // A set of 2 or more nodes + }; + + enum Flags : UCHAR + { + FLAG_NONE = 0, + FLAG_METHOD_ARGUMENT = 1, + FLAG_SOLID = 2, // in brackets + }; + + template + requires(T != JsonExprNode::Type::METHOD_NODE && T != JsonExprNode::Type::VARIABLE_NODE) + static JsonExprNode* make(MemoryPool& pool, const UCHAR flag = FLAG_NONE) + { + return FB_NEW_POOL(pool) JsonExprNode(pool, T); + } + + // It is better to use `make` function to create a new node + explicit JsonExprNode(MemoryPool& pool, const Type type = CALCULATION_NODE, const UCHAR flag = FLAG_NONE); + + // Variable node + JsonExprNode(MemoryPool& pool, PathVariable* var); + + // Method node + JsonExprNode(MemoryPool& pool, const PathMethod method); + + JsonExprNode(JsonExprNode&&) noexcept = delete; + JsonExprNode(const JsonExprNode&) = delete; + + JsonExprNode& operator=(JsonExprNode&&) noexcept = delete; + JsonExprNode& operator=(const JsonExprNode&) = delete; + + ~JsonExprNode(); + +public: + void passPassing(const PassingList* passing); + +public:// Children routines + JsonExprNode* addChild(const Type childType, const Flags flags = FLAG_NONE); + + // The incoming node cloud be modifed and deleted (the function returns false in this case) + bool addChild(JsonExprNode* node); + + // Only for CALCULATION_NODE + void addOperation(const JsonExprOperation operation); + + // Value methods + inline JsonExprNode* addVariable(PathVariable* main) + { + JsonExprNode* currentLevel = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), main); + return addChild(currentLevel) ? currentLevel : nullptr; + } + + // Make a new node and insert it as first element + void makeHeadNode(const bool unwrap); + + inline void setHead(JsonExprNode* base) + { + fb_assert(canAccessChildren()); + fb_assert(m_data.childrenBlock->children[0] == nullptr || m_data.childrenBlock->children[0]->m_type == VARIABLE_NODE); + m_data.childrenBlock->children[0] = base; + } + + // Call it after all addChild, when all operators and values are added + JsonExprNode* finish(); + +public: // Set content methods + inline JsonExprNode* set(const int value) noexcept + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.set(value); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + + inline JsonExprNode* set(const SINT64 value) noexcept + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.set(value); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + + inline JsonExprNode* set(const double value) noexcept + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.set(value); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + + inline JsonExprNode* set(const bool value) noexcept + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.set(value); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + + inline JsonExprNode* set(const char* value) noexcept + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.set(value); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + + inline JsonExprNode* setNull() noexcept + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.setToNull(); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + + inline JsonExprNode* set(const SmallString& str) + { + fb_assert(m_type == JsonExprNode::SCALAR_NODE); + + m_data.scalarBlock->value.set(str); + m_type = JsonExprNode::SCALAR_NODE; + return this; + } + +public: // Getters + inline JsonExprNode* getFirstChild() const + { + fb_assert(canAccessChildren()); + fb_assert(m_data.childrenBlock->children.getCount() > 0); + return m_data.childrenBlock->children[0]; + } + + inline JsonExprNode* getSecondChild() const + { + fb_assert(canAccessChildren()); + fb_assert(m_data.childrenBlock->children.getCount() > 1); + return m_data.childrenBlock->children[1]; + } + + inline JsonExprNode* getLastChild() const + { + fb_assert(canAccessChildren()); + fb_assert(m_data.childrenBlock->children.getCount() > 0); + return m_data.childrenBlock->children[m_data.childrenBlock->children.getCount() - 1]; + } + + inline FB_SIZE_T getChildrenCount() const noexcept + { + if (!canAccessChildren()) + return 0; + + return m_data.childrenBlock->children.getCount(); + } + + inline void clearChildren() noexcept + { + fb_assert(canAccessChildren()); + m_data.childrenBlock->children.clear(); + } + +public:// Checkers + inline bool isRootEmpty() const + { + switch (m_type) + { + case JsonExprNode::CALCULATION_NODE: + { + auto& children = m_data.calculationBlock->children; + auto& operations = m_data.calculationBlock->operations; + return (children.isEmpty() || children.getCount() == 1 && children[0]->isRootEmpty()) && operations.isEmpty(); + } + case JsonExprNode::COMPOUND_NODE: + case JsonExprNode::FILTER_NODE: + return m_data.childrenBlock->children.isEmpty(); + default: + return false; + } + } + + inline bool isVariable() const + { + return m_type == JsonExprNode::VARIABLE_NODE; + } + + inline bool testType(const JsonExprNode::Type type) const + { + return this->m_type == type; + } + + inline bool canHasTail() const + { + switch (m_type) + { + case JsonExprNode::METHOD_NODE: + case JsonExprNode::VARIABLE_NODE: + case JsonExprNode::FILTER_NODE: + return true; + default: + return false; + } + } + + inline bool hasTail() const + { + switch (m_type) + { + case METHOD_NODE: + return m_data.childrenBlock->children.getCount() >= 2 && (m_data.childrenBlock->children.back()->m_flags & FLAG_METHOD_ARGUMENT) == 0; + case VARIABLE_NODE: + return m_data.childrenBlock->children.getCount() == 1; + case FILTER_NODE: + return m_data.childrenBlock->children.getCount() == 3; + default: + return false; + } + } + + inline JsonExprNode* getTailNode() const + { + if (hasTail()) + return m_data.childrenBlock->children.back(); // Always the last + else + return nullptr; + } + + inline PathMethod getPathMethod() const noexcept + { + if (!testType(METHOD_NODE)) + return PathMethod::NONE; + + return m_data.methodBlock->method; + } + + const PathVariable* getVariable() const; + + inline bool applyUnaryOp(const JsonExprOperation op) noexcept + { + fb_assert(op == JsonExprOperation::UNARY_MINUS || op == JsonExprOperation::UNARY_PLUS); + + if (!testType(SCALAR_NODE)) + return false; + + if (op == JsonExprOperation::UNARY_PLUS) + return true; + + if (m_data.scalarBlock->value.getType() == ValueType::INT) + { + m_data.scalarBlock->value.getValue().integer = -m_data.scalarBlock->value.getValue().integer; + return true; + } + else + { + m_data.scalarBlock->value.getValue().doubleValue = -m_data.scalarBlock->value.getValue().doubleValue; + return true; + } + } + + std::optional getRangeNumber() const + { + if (!testType(SCALAR_NODE)) + return std::nullopt; + + static constexpr RangeSize rangeMin = std::numeric_limits::min(); + static constexpr RangeSize rangeMax = std::numeric_limits::max(); + + auto invalidRangeOrTypeError = []() + { + json_fatal_exception::raise(JsonStatusMsgWrapper(isc_jpath_common) << + JsonStatusMsgWrapper(isc_jpath_invalid_range) << rangeMin << rangeMax); + }; + + if (!m_data.scalarBlock->value.isNumber()) + invalidRangeOrTypeError(); + + if (m_data.scalarBlock->value.getType() != ValueType::INT) + invalidRangeOrTypeError(); + + const auto value = m_data.scalarBlock->value.getValue().integer; + if (value < rangeMin || value > rangeMax) + invalidRangeOrTypeError(); + + + return static_cast(value); + } + + // A calculation node with only one child is useless in term of this node + // So it is safe to omit such node and keep only the child + bool canBeOmitted() const + { + return m_type == CALCULATION_NODE && getChildrenCount() == 1 && !m_data.calculationBlock->operations.hasData(); + } + +public: // Helpers + // Unwrap path in a variable node + void unwrap(); + + constexpr void addFlag(const Flags flag) + { + m_flags |= UCHAR(flag); + } + +public: // Executions + enum class FilterResult + { + // Add the 'R' prefix to avoid conflicts with macros + RTRUE, + RFALSE, + RUNKNOWN + }; + //static FilterResult passFilter(const ContextVariables& context, const JsonExprNode* filter); + + // Executers + //JsonVariant execute(const ContextVariables& context) const; + +private: + inline bool canAccessChildren() const noexcept + { + // Basically all except SCALAR_NODE + return m_type == CALCULATION_NODE || m_type == FILTER_NODE || m_type == METHOD_NODE || + m_type == VARIABLE_NODE || m_type == COMPOUND_NODE; + } + + //JsonVariant executeVariableNode(const ContextVariables& context) const; + //JsonVariant executeCalculationNode(const ContextVariables& context) const; + //JsonVariant executeMethodNode(const ContextVariables& context) const; + //JsonVariant executeFilterNode(const ContextVariables& context) const; + +private: + // Scalar node + + struct ScalarBlock + { + JsonScalar value; + + ScalarBlock(MemoryPool& pool) : + value(pool) + { } + }; + + + struct ChildrenNodes + { + Firebird::HalfStaticArray children; + + ChildrenNodes(MemoryPool& pool) : + children(pool) + { } + }; + + struct VariableBlock : public ChildrenNodes + { + VariableBlock(MemoryPool& pool) : + ChildrenNodes(pool) + { } + + Firebird::AutoPtr variable; //$. or @. or $pass + //JsonVariant* cachedVariable = nullptr; + }; + + struct CalculationBlock : public ChildrenNodes + { + CalculationBlock(MemoryPool& pool) : + ChildrenNodes(pool), + operations(pool) + { } + + Firebird::HalfStaticArray operations; + // Counter of each operation for correct execution order + OffsetCounter offsets = {}; + }; + + // Variable node + struct MethodBlock : public ChildrenNodes + { + // Method node + PathMethod method = PathMethod::NONE; + MethodBlock(MemoryPool& pool) : + ChildrenNodes(pool) + { } + }; + + struct FilterBlock : public ChildrenNodes + { + FilterBlock(MemoryPool& pool) : + ChildrenNodes(pool) + { } + }; + + union NodeData + { + ScalarBlock* scalarBlock; + VariableBlock* varBlock; + ChildrenNodes* childrenBlock; + CalculationBlock* calculationBlock; + MethodBlock* methodBlock; + FilterBlock* filterBlock; + } m_data{}; + + // Common data + Type m_type; + UCHAR m_flags{}; +}; + +} + +#endif // JSON_RUNTIME_H diff --git a/src/jrd/json/JsonUtils.h b/src/jrd/json/JsonUtils.h new file mode 100644 index 00000000000..054be3401ad --- /dev/null +++ b/src/jrd/json/JsonUtils.h @@ -0,0 +1,110 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonUtils.h + * DESCRIPTION: Common functions to work with JSON. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2025 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_UTILS_H +#define JSON_UTILS_H + +#include "firebird.h" +#include "../../common/StatusArg.h" + +#include +#include // std::to_chars +#include + +#ifdef ANDROID +#include "../common/utils_proto.h" +#endif + + +namespace FBJSON +{ + +using NumberConvertBuffer = std::array; + +inline constexpr int NUMBER_PRECISION = 14; + +template +inline std::string_view convertNumberToString(NumberConvertBuffer& buffer, T value) +{ + // Android does not have to_chars +#ifdef ANDROID + ULONG outputLength = 0; + + if constexpr (std::is_floating_point_v) + { + outputLength = fb_utils::snprintf(buffer.data(), buffer.size(), "%.*g", NUMBER_PRECISION, value); + + // Replace the ',' with '.' + for (char* it = buffer.begin(); it != buffer.end(); ++it) + { + if (*it == ',') + { + *it = '.'; + break; + } + } + } + else + outputLength = fb_utils::snprintf(buffer.data(), buffer.size(), "%" SQUADFORMAT, value); + + return std::string_view(buffer.data(), outputLength); +#else + //! Use to_chars because other function are locale-depended and can insert a ',' instead of a '.' + std::to_chars_result result; + + //! Do not use begin/end or front/back because they requeue some obscure extra casts to char* on Windows + if constexpr (std::is_floating_point_v) + result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value, std::chars_format::general, NUMBER_PRECISION); + else + result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + + if (result.ec != std::errc()) + { + fb_assert(false); + Firebird::Arg::Gds(isc_arith_except).raise(); + } + + return std::string_view(buffer.data(), result.ptr - buffer.data()); +#endif +} + +template +constexpr USHORT getSuperType(T op1, T op2) +{ + return (static_cast(op1) << 8) | static_cast(op2); +} + +enum BoolSuperType : USHORT +{ + TRUE_VS_FALSE = getSuperType(true, false), + FALSE_VS_TRUE = getSuperType(false, true), + TRUE_VS_TRUE = getSuperType(true, true), + FALSE_VS_FALSE = getSuperType(false, false) +}; + +} // namespace + +#endif // !JSON_UTILS_H diff --git a/src/jrd/json/classes/JsonDatetime.cpp b/src/jrd/json/classes/JsonDatetime.cpp new file mode 100644 index 00000000000..84039baf7e3 --- /dev/null +++ b/src/jrd/json/classes/JsonDatetime.cpp @@ -0,0 +1,338 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonDatetime.cpp + * DESCRIPTION: Common types for JSON code. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2024 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + + +#include "JsonDatetime.h" +#include "../JsonConsts.h" + +// Engine stuff for datetime parsing +#include "../common/cvt.h" +#include "../common/dsc.h" +#include "../common/TimeZoneUtil.h" +#include "../common/CvtFormat.h" +#include "../common/classes/BlrReader.h" +#include "../common/classes/BlrWriter.h" + +#include "../jrd/cvt_proto.h" +#include "../jrd/mov_proto.h" + +namespace FBJSON{ + +using DATETIME_SUBTYPE = Firebird::EXPECT_DATETIME; + + +// Use custom throw function because EngineCallbacks::instance make tdbb_status dirty +class JsonErrorCallback : public Jrd::EngineCallbacks +{ +public: + JsonErrorCallback(MemoryPool&) : + Jrd::EngineCallbacks(throwError) + { } + + [[noreturn]] static void throwError(const Firebird::Arg::StatusVector& v) + { + v.raise(); + } +}; +Firebird::GlobalPtr jsonCallback; + +struct DatetimeUtils +{ + ISC_TIMESTAMP_TZ inputUtcTimestamp; + ISC_TIMESTAMP localBuffer; + ISC_TIME_TZ timeTzBuffer; + + static DATETIME_SUBTYPE getFormat(const Firebird::CvtStringContains::TypeFlags flags) + { + using namespace std::literals; + using namespace Firebird::CvtStringContains; + + switch (flags) + { + case TIME | TIMEZONE: + return DATETIME_SUBTYPE::expect_sql_time_tz; + case TIME: + return DATETIME_SUBTYPE::expect_sql_time; + case DATE | TIMEZONE: + case DATE | TIME | TIMEZONE: + return DATETIME_SUBTYPE::expect_timestamp_tz; + case DATE | TIME: + return DATETIME_SUBTYPE::expect_timestamp; + case DATE: + return DATETIME_SUBTYPE::expect_sql_date; + default: + fb_assert(false); + return DATETIME_SUBTYPE::expect_sql_time_tz; + } + } + + ISC_TIMESTAMP& getLocal() + { + return localBuffer = Firebird::TimeZoneUtil::timeStampTzToTimeStamp(inputUtcTimestamp, + JRD_get_thread_data()->getAttachment()->att_current_timezone); + } + + dsc makeDatetimeDsc(const ISC_TIMESTAMP_TZ utcTimestamp, const JsonDatetime::Flags flags) + { + inputUtcTimestamp = utcTimestamp; + + dsc timestampDsc; + + switch (getFormat(flags)) + { + case DATETIME_SUBTYPE::expect_sql_time: + timestampDsc.makeTime(&getLocal().timestamp_time); + break; + case DATETIME_SUBTYPE::expect_sql_time_tz: + timeTzBuffer.utc_time = utcTimestamp.utc_timestamp.timestamp_time; + timeTzBuffer.time_zone = utcTimestamp.time_zone; + timestampDsc.makeTimeTz(&timeTzBuffer); + break; + case DATETIME_SUBTYPE::expect_sql_date: + timestampDsc.makeDate(&getLocal().timestamp_date); + break; + case DATETIME_SUBTYPE::expect_timestamp: + timestampDsc.makeTimestamp(&getLocal()); + break; + case DATETIME_SUBTYPE::expect_timestamp_tz: + timestampDsc.makeTimestampTz(&inputUtcTimestamp); + break; + default: + fb_assert(false); + timestampDsc.makeTimestampTz(&inputUtcTimestamp); + break; + } + + return timestampDsc; + } +}; + + +void JsonDatetime::set(Jrd::thread_db* tdbb, const dsc& valueDsc) +{ + using namespace Firebird::CvtStringContains; + m_stringRepresentation = MOV_make_string2(tdbb, &valueDsc, JSON_TTYPE); + + // m_ts = MOV_get_timestamp_tz(&valueDsc); + // Can't use MOV_get_timestamp_tz because the CVT_string_to_datetime function adds TimeZoneUtil::TIME_TZ_BASE_DATE + // and other instance stuff. It is more reliably to just use the same function instead of tring to mimic its output + + Firebird::EXPECT_DATETIME format = DATETIME_SUBTYPE::expect_timestamp_tz; + switch (valueDsc.dsc_dtype) + { + case dtype_sql_time: + format = DATETIME_SUBTYPE::expect_sql_time; + break; + case dtype_sql_time_tz: + case dtype_ex_time_tz: + format = DATETIME_SUBTYPE::expect_sql_time_tz; + break; + case dtype_sql_date: + format = DATETIME_SUBTYPE::expect_sql_date; + break; + case dtype_timestamp: + format = DATETIME_SUBTYPE::expect_timestamp; + break; + case dtype_timestamp_tz: + case dtype_ex_timestamp_tz: + format = DATETIME_SUBTYPE::expect_timestamp_tz; + break; + default: + fb_assert(false); + } + + m_ts = convertToTimeStamp(m_stringRepresentation, format, m_flags); +} + +void JsonDatetime::updatePrecision(const USHORT precision) +{ + using namespace Firebird; + + TimeStamp::round_time(m_ts.utc_timestamp.timestamp_time, precision); + + DatetimeUtils maker; + dsc timestampDsc = maker.makeDatetimeDsc(m_ts, m_flags); + + m_stringRepresentation = MOV_make_string2(JRD_get_thread_data(), ×tampDsc, JSON_TTYPE); + m_stringRepresentation.rtrim(); +} + +std::string_view JsonDatetime::getTypeName() const +{ + using namespace std::literals; + using namespace Firebird::CvtStringContains; + using namespace Firebird; + + switch (getFormat()) + { + case DATETIME_SUBTYPE::expect_sql_time_tz: + return Tokens::TypeName::TIME_WITH_TZ; + case DATETIME_SUBTYPE::expect_sql_time: + return Tokens::TypeName::TIME_WITHOUT_TZ; + case DATETIME_SUBTYPE::expect_timestamp_tz: + return Tokens::TypeName::TIMESTAMT_WITH_TZ; + case DATETIME_SUBTYPE::expect_timestamp: + return Tokens::TypeName::TIMESTAMT_WITHOUT_TZ; + case DATETIME_SUBTYPE::expect_sql_date: + return Tokens::TypeName::DATE; + default: + fb_assert(false); + return Tokens::TypeName::UNKNOWN; + } +} + +Firebird::EXPECT_DATETIME JsonDatetime::getFormat() const +{ + return DatetimeUtils::getFormat(m_flags); +} + +void JsonDatetime::storeAsBytes(Firebird::BlrWriter& array) const +{ + array.appendBytes(reinterpret_cast(&m_ts), TIMEZONE_SIZE); + array.appendMetaString(m_stringRepresentation.c_str()); + array.appendUChar(m_flags); +} + +void JsonDatetime::readFromBytes(Firebird::BlrReader& array) +{ + const UCHAR* pos = array.getPos(); + array.seekForward(TIMEZONE_SIZE); + m_ts = *reinterpret_cast(pos); + + array.getString(m_stringRepresentation); + m_flags = static_cast(array.getByte()); +} + +bool JsonDatetime::operator>(const JsonDatetime& right) const +{ + return Firebird::TimeZoneUtil::compareUtcTimeStamps(m_ts.utc_timestamp, right.m_ts.utc_timestamp) > 0; +} + +bool JsonDatetime::operator>=(const JsonDatetime& right) const +{ + return Firebird::TimeZoneUtil::compareUtcTimeStamps(m_ts.utc_timestamp, right.m_ts.utc_timestamp) >= 0; +} + +bool JsonDatetime::operator==(const JsonDatetime& right) const +{ + return Firebird::TimeZoneUtil::compareUtcTimeStamps(m_ts.utc_timestamp, right.m_ts.utc_timestamp) == 0; +} + +bool JsonDatetime::operator<(const JsonDatetime& right) const +{ + return !operator>=(right); +} + +bool JsonDatetime::operator<=(const JsonDatetime& right) const +{ + return !operator>(right); +} + +bool JsonDatetime::operator!=(const JsonDatetime& right) const +{ + return !operator==(right); +} + + +ISC_TIMESTAMP_TZ JsonDatetime::convertToTimeStamp(const std::string_view source, const std::string_view format, + Flags& outFlags, Firebird::Callbacks* callback) +{ + using namespace Firebird; + + fb_assert(source.length() < MAX_USHORT); + + outFlags = Firebird::CvtStringContains::NONE; + + dsc strdesc; + strdesc.makeText(static_cast(source.length()), CS_NONE, (UCHAR*)source.data()); + + // CVT_string_to_datetime throws exceptions not only via callback, so setting a custom ErrorFunction is not an option + if (callback == nullptr) + callback = &jsonCallback; + + ISC_TIMESTAMP_TZ dest = {}; + + // CVT functions return timestamp in UTC + if (format.length()) + { + // Expected type does not matter + // Exceptions only on invalid input + dest = CVT_format_string_to_datetime(&strdesc, + string(format.data(), static_cast(format.length())), + DATETIME_SUBTYPE::expect_timestamp_tz, callback, &outFlags); + } + else + { + // We need to detect the type so first try to parse the string as timestamp + // On failure - try sql_time + // If format is invalid - catch the exception in the JsonVariant::convertToDatetime method + try + { + CVT_string_to_datetime(&strdesc, &dest, &outFlags, + EXPECT_DATETIME::expect_timestamp_tz, true, callback); + } + catch (...) + { + dest = {}; + CVT_string_to_datetime(&strdesc, &dest, &outFlags, + Firebird::EXPECT_DATETIME::expect_sql_time_tz, true, callback); + } + + // When passing incorrect expected type (for example, expect_timestamp_tz for a date), + // the source type will be adjusted to the expected + // Use flags to get the real output type and extract correct value + const auto realType = DatetimeUtils::getFormat(outFlags); + dest = {}; + CVT_string_to_datetime(&strdesc, &dest, &outFlags, realType, true, callback); + } + + return dest; +} + +ISC_TIMESTAMP_TZ JsonDatetime::convertToTimeStamp(const std::string_view source, const Firebird::EXPECT_DATETIME format, + Flags& outFlags, Firebird::Callbacks* callback) +{ + using namespace Firebird; + + fb_assert(source.length() < MAX_USHORT); + + outFlags = Firebird::CvtStringContains::NONE; + + // Pass string as dsc + dsc strdesc; + strdesc.makeText(static_cast(source.length()), CS_NONE, (UCHAR*)source.data()); + + // CVT_string_to_datetime throws exceptions not only via callback, so setting a custom ErrorFunction is not an option + if (callback == nullptr) + callback = &jsonCallback; + + ISC_TIMESTAMP_TZ timeStamp = {}; + CVT_string_to_datetime(&strdesc, &timeStamp, &outFlags, format, true, callback); + + return timeStamp; +} + +} diff --git a/src/jrd/json/classes/JsonDatetime.h b/src/jrd/json/classes/JsonDatetime.h new file mode 100644 index 00000000000..b90104d1449 --- /dev/null +++ b/src/jrd/json/classes/JsonDatetime.h @@ -0,0 +1,138 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonDatetime.h + * DESCRIPTION: Common types for JSON code. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2024 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_DATETIME_H +#define JSON_DATETIME_H + +#include "firebird.h" +#include "fb_exception.h" +#include "../../common/cvt.h" + +#include "../JsonConsts.h" + +#include + +namespace Firebird { + // Use this for serialization because they have helpful put/get fuctions + class BlrReader; + class BlrWriter; +} + +namespace Jrd { + class thread_db; +} + +namespace FBJSON { + +class JsonDatetime +{ +public: + using Flags = Firebird::CvtStringContains::TypeFlags; + + JsonDatetime(Firebird::MemoryPool& pool) : m_stringRepresentation(pool) + { } + + JsonDatetime(Firebird::MemoryPool& pool, const JsonDatetime& rhs) : m_stringRepresentation(pool) + { + m_stringRepresentation = rhs.m_stringRepresentation; + m_ts = rhs.m_ts; + m_flags = rhs.m_flags; + } + + void set(const std::string_view source, std::string_view format = "", Firebird::Callbacks* callback = nullptr) + { + m_ts = convertToTimeStamp(source, format, m_flags, callback); + m_stringRepresentation = source; + } + + void set(SmallString&& source, const std::string_view format) + { + m_ts = convertToTimeStamp(source, format, m_flags); + m_stringRepresentation = std::move(source); + } + + void set(Jrd::thread_db* tdbb, const dsc& valueDsc); + + void set(const std::string_view source, const Firebird::EXPECT_DATETIME format) + { + m_ts = convertToTimeStamp(source, format, m_flags); + m_stringRepresentation = source; + } + + void updateFormat(const std::string_view format, Firebird::Callbacks* callback = nullptr) + { + m_ts = convertToTimeStamp(m_stringRepresentation, format, m_flags, callback); + } + + void updatePrecision(const USHORT precision); + + SmallString&& extractString() + { + return std::move(m_stringRepresentation); + } + + std::string_view asString() const + { + return m_stringRepresentation; + } + + std::string_view getTypeName() const; + inline ISC_TIMESTAMP_TZ getTS() const noexcept + { + return m_ts; + } + + Firebird::EXPECT_DATETIME getFormat() const; + void storeAsBytes(Firebird::BlrWriter& array) const; + void readFromBytes(Firebird::BlrReader& array); + + bool operator>(const JsonDatetime& right) const; + bool operator>=(const JsonDatetime& right) const; + bool operator==(const JsonDatetime& right) const; + bool operator<(const JsonDatetime& right) const; + bool operator<=(const JsonDatetime& right) const; + bool operator!=(const JsonDatetime& right) const; + +private: + static constexpr FB_SIZE_T TIMEZONE_SIZE = sizeof(ISC_TIMESTAMP_TZ); + +private: + static ISC_TIMESTAMP_TZ convertToTimeStamp(const std::string_view source, const std::string_view format, + Flags& flags, Firebird::Callbacks* callback = nullptr); + + static ISC_TIMESTAMP_TZ convertToTimeStamp(const std::string_view source, const Firebird::EXPECT_DATETIME format, + Flags& flags, Firebird::Callbacks* callback = nullptr); + + SmallString m_stringRepresentation; + + // The TS contains datetime in UTC and the source timezone + ISC_TIMESTAMP_TZ m_ts = {}; + Flags m_flags = Firebird::CvtStringContains::NONE; +}; + +} + +#endif // JSON_DATETIME_H diff --git a/src/jrd/json/classes/JsonScalar.cpp b/src/jrd/json/classes/JsonScalar.cpp new file mode 100644 index 00000000000..18ab6b610e5 --- /dev/null +++ b/src/jrd/json/classes/JsonScalar.cpp @@ -0,0 +1,253 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonScalar.cpp + * DESCRIPTION: A scalar variant type used in JSON code. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2026 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + + +#include "JsonScalar.h" + +#include "../JsonConsts.h" +#include "JsonTypes.h" +#include "JsonDatetime.h" + +#include "firebird.h" +#include "../common/classes/BlrReader.h" +#include "../common/classes/BlrWriter.h" + +using namespace FBJSON; + + +JsonScalar& JsonScalar::operator=(const JsonScalar& rhs) +{ + reset(rhs.type); + flags = rhs.flags; + + switch (rhs.type) + { + case ValueType::STRING: + value.string = FB_NEW_POOL(getPool()) SmallString(getPool(), *rhs.value.string); + break; + case ValueType::DATETIME: + value.datetime = FB_NEW_POOL(getPool()) JsonDatetime(getPool(), *rhs.value.datetime); + break; + default: + memcpy(&value, &rhs.value, sizeof(DataVariant)); + break; + } + return *this; +} + +double JsonScalar::getDouble() const +{ + switch (type) + { + case ValueType::INT: + return static_cast(value.integer); + case ValueType::DOUBLE: + return value.doubleValue; + case ValueType::STRING: + json_skippable_exception::raise(JsonStatusMsg(isc_jdyn_common) << + JsonStatusMsg(isc_jdyn_get_double_string_type_error) << JsonStatusMsgStrArg(*value.string)); + default: + json_skippable_exception::raise(JsonStatusMsg(isc_jdyn_common) << + JsonStatusMsg(isc_jdyn_get_double_invalid_type_error)); + } + + // Just in case + fb_assert(false); + return 0; +} + +SmallString* JsonScalar::str() const +{ + if (type == ValueType::STRING) + return value.string; + else + { + fb_assert(false); + json_skippable_exception::raise(JsonStatusMsg(isc_jdyn_common) << + JsonStatusMsg(isc_jdyn_internal_error)); + return nullptr; + } +} + +void JsonScalar::set(const std::string_view view) +{ + // Create the new string first in case the new range belongs to the current string + SmallString* temp = FB_NEW_POOL(getPool()) SmallString(getPool(), view.data(), static_cast(view.length())); + reset(ValueType::STRING); + + value.string = temp; +} + +JsonDatetime& JsonScalar::setToDatetime() +{ + reset(ValueType::DATETIME); + + value.datetime = FB_NEW_POOL(getPool()) JsonDatetime(getPool()); + return *value.datetime; +} + +void JsonScalar::writeScalarAsBytes(Firebird::BlrWriter& array) const +{ + static constexpr USHORT ushortSizeInBits = sizeof(USHORT) * CHAR_BIT; + array.appendUChar(static_cast(type)); + + switch (type) + { + case ValueType::JNULL: + break; + case ValueType::BOOL: + array.appendUChar(value.boolean ? 1 : 0); + break; + case ValueType::INT: + array.appendUShort(value.integer); + array.appendUShort(value.integer >> ushortSizeInBits); + array.appendUShort(value.integer >> ushortSizeInBits * 2); + array.appendUShort(value.integer >> ushortSizeInBits * 3); + break; + case ValueType::DOUBLE: + { + SINT64 i64value = *((SINT64*)(&value.doubleValue)); + array.appendUShort(i64value); + array.appendUShort(i64value >> ushortSizeInBits); + array.appendUShort(i64value >> ushortSizeInBits * 2); + array.appendUShort(i64value >> ushortSizeInBits * 3); + break; + } + case ValueType::STRING: + array.appendString(1, *value.string); + break; + case ValueType::DATETIME: + value.datetime->storeAsBytes(array); + break; + default: + fb_assert(false); + break; + } +} + +void JsonScalar::readScalarFromBytes(Firebird::BlrReader& array) +{ + const ValueType rtype = static_cast(array.getByte()); + + switch (rtype) + { + case ValueType::JNULL: + break; + case ValueType::BOOL: + set(array.getByte() == 1); + break; + case ValueType::INT: + { + const SSHORT len = sizeof(SINT64); + SINT64 buffer = isc_portable_integer(array.getPos(), len); + array.seekForward(len); + + set(buffer); + break; + } + case ValueType::DOUBLE: + { + const SSHORT len = sizeof(SINT64); + SINT64 buffer = isc_portable_integer(array.getPos(), len); + array.seekForward(len); + + double dval = *((double*)(&buffer)); + set(dval); + break; + } + case ValueType::STRING: + { + set(""); + array.getVerbString(*value.string); + break; + } + case ValueType::DATETIME: // keep it as the last scalar type + { + FBJSON::JsonDatetime& datetime = setToDatetime(); + datetime.readFromBytes(array); + break; + } + default: + fb_assert(false); + } +} + +Jrd::impure_value JsonScalar::makeScalarDsc() const +{ + Jrd::impure_value output = {}; + switch (type) + { + case ValueType::JNULL: + output.vlu_desc.setNull(); + break; + case ValueType::BOOL: + output.vlu_misc.vlu_uchar = value.boolean; + output.vlu_desc.makeBoolean(reinterpret_cast(&output.vlu_misc.vlu_uchar)); + break; + case ValueType::INT: + output.make_int64(value.integer); + break; + case ValueType::DOUBLE: + output.make_double(value.doubleValue); + break; + case ValueType::STRING: + output.vlu_desc.makeText(value.string->length(), JSON_TTYPE, (UCHAR*)value.string->data()); + break; + case ValueType::DATETIME: // keep it as the last scalar type + output.vlu_misc.vlu_timestamp_tz = value.datetime->getTS(); + output.vlu_desc.makeTimestampTz(&output.vlu_misc.vlu_timestamp_tz); + break; + default: + fb_assert(false); + } + + return output; +} + +void JsonScalar::reset(const ValueType newType) noexcept +{ + switch (type) + { + case ValueType::EMPTY: + case ValueType::BOOL: + case ValueType::INT: + case ValueType::JNULL: + break; + case ValueType::STRING: + delete value.string; + break; + case ValueType::DATETIME: + delete value.datetime; + break; + default: + releaseExtend(); + break; + } + flags = FLAG_NONE; + + value = {}; + type = newType; +} diff --git a/src/jrd/json/classes/JsonScalar.h b/src/jrd/json/classes/JsonScalar.h new file mode 100644 index 00000000000..2735594be1f --- /dev/null +++ b/src/jrd/json/classes/JsonScalar.h @@ -0,0 +1,306 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonScalar.h + * DESCRIPTION: A scalar variant type used in JSON code. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2026 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + + +#ifndef JSON_SCALAR_H +#define JSON_SCALAR_H + +#include "../JsonConsts.h" + +#include "../jrd/val.h" + +#include + +namespace Firebird { + // Use this for serialization because they have helpful put/get fuctions + class BlrReader; + class BlrWriter; +} + +namespace FBJSON { + +class JsonDatetime; +class OutputJsonText; +class Jsonb; +class JsonbView; + +// All possible types during runtime calculation +enum class ValueType : UCHAR +{ + EMPTY = 0, + ERROR_VALUE, + UNKNOWN, + + // * JsonScalar scope begin + JNULL, + BOOL, + INT, + //JFLOAT, + DOUBLE, + STRING, + DATETIME, // keep it as the last scalar type + // * JsonScalar scope end + + JSON_TEXT, + JSON_BINARY, + JSON_BINARY_VIEW +}; +constexpr std::pair SCALAR_WITH_NULL_TYPES_RANGE = {ValueType::JNULL, ValueType::DATETIME}; +constexpr std::pair SCALAR_TYPES_RANGE = {ValueType::BOOL, ValueType::DATETIME}; + +// All possible values to store during runtime calculation +union DataVariant +{ + // Scalars + bool boolean; + SINT64 integer; + double doubleValue; + SmallString* string; + JsonDatetime* datetime; + + // Runtime values, not used in JsonScalar + //OutputJsonText* json; // temp comment + //Jsonb* jsonb; // temp comment + //JsonbView* jsonView; // temp comment +}; +static_assert(sizeof(DataVariant) <= 8, "Big types should be stored as pointers to save memory"); + +// A storage to keep parsed JSON scalar and NULL value +// The scalar has the ability to be stored in a byted representation +class JsonScalar : public Firebird::PermanentStorage +{ +public: + + enum Flags : UCHAR + { + FLAG_NONE = 0, + FLAG_HAS_QUOTES = 1 + }; + + class StringView; + +public: + JsonScalar(MemoryPool& pool) : Firebird::PermanentStorage(pool) + { } + + JsonScalar(JsonScalar&& rhs) : Firebird::PermanentStorage(rhs.getPool()) + { + operator=(std::forward(rhs)); + } + + JsonScalar(const JsonScalar& rhs) : Firebird::PermanentStorage(rhs.getPool()) + { + operator=(rhs); + } + + JsonScalar(MemoryPool& pool, const JsonScalar& rhs) : Firebird::PermanentStorage(pool) + { + operator=(rhs); + } + + JsonScalar& operator=(const JsonScalar& rhs); + + JsonScalar& operator=(JsonScalar&& rhs) + { + type = rhs.type; + flags = rhs.flags; + memcpy(&value, &rhs.value, sizeof(DataVariant)); + rhs.value = {}; + + rhs.type = ValueType::JNULL; + return *this; + } + + virtual ~JsonScalar() + { + reset(ValueType::JNULL); + } + +public: // Getters + const DataVariant& getValue() const noexcept + { + return value; + }; + + DataVariant& getValue() noexcept + { + return value; + }; + + + constexpr ValueType getType() const noexcept + { + return type; + } + + double getDouble() const; + + inline std::string_view getStringView() const + { + SmallString* asString = str(); + return std::string_view(asString->data(), asString->length()); + } + + SmallString* str() const; + +public: // Checkers + constexpr bool hasFlag(const Flags flag) const noexcept + { + return flags & flag; + } + + constexpr bool isScalar() const noexcept + { + return SCALAR_TYPES_RANGE.first <= type && type <= SCALAR_TYPES_RANGE.second; + } + + constexpr bool isScalarOrNull() const noexcept + { + return SCALAR_WITH_NULL_TYPES_RANGE.first <= type && type <= SCALAR_WITH_NULL_TYPES_RANGE.second; + } + + inline bool isNumber() const noexcept + { + return type == ValueType::INT || type == ValueType::DOUBLE; + } + + constexpr bool isString() const noexcept + { + return type == ValueType::STRING; + } + + constexpr bool isNull() const noexcept + { + return type == ValueType::JNULL; + } + + constexpr bool isTrue() const noexcept + { + return type == ValueType::BOOL && value.boolean; + } + +public: // Setters + constexpr void addFlag(const Flags flag) noexcept + { + flags |= flag; + } + + inline void setToNull() + { + reset(ValueType::JNULL); + } + + inline void set(const bool boolValue) + { + reset(ValueType::BOOL); + value.boolean = boolValue; + } + + inline void set(const int intValue) + { + reset(ValueType::INT); + value.integer = intValue; + } + + inline void set(const SINT64 bigIntValue) + { + reset(ValueType::INT); + value.integer = bigIntValue; + } + + inline void set(const double doublevalue) + { + reset(ValueType::DOUBLE); + value.doubleValue = doublevalue; + } + + //! Add explicit overload for c-string to avoid casting const char* to bool + void set(const char* const view) + { + set(std::string_view(view)); + } + + void set(const std::string_view view); + + //! Remove random pointer to bool casts + template + void set(T*) = delete; + + JsonDatetime& setToDatetime(); + +public: // Serialization + void writeScalarAsBytes(Firebird::BlrWriter& array) const; + void readScalarFromBytes(Firebird::BlrReader& array); + + Jrd::impure_value makeScalarDsc() const; + +protected: + void reset(const ValueType newType) noexcept; + + // Used in JsonVariant to release json/jsonb pointers + virtual void releaseExtend() + { } + + // NOLINTBEGIN(misc-non-private-member-variables-in-classes) +protected: + DataVariant value = {}; + ValueType type = ValueType::JNULL; + UCHAR flags = FLAG_NONE; // Only runtime + // NOLINTEND(misc-non-private-member-variables-in-classes) +}; + +class JsonScalar::StringView +{ +public: + StringView(MemoryPool& pool, SmallString* stringToSet) : + m_scalar(pool) + { + m_scalar.value.string = stringToSet; + m_scalar.type = ValueType::STRING; + } + + StringView(const StringView& ref) = delete; + StringView(StringView&& ref) = delete; + + auto operator=(const StringView&) = delete; + auto operator=(StringView&&) = delete; + + ~StringView() + { + m_scalar.type = ValueType::EMPTY; + } + + const JsonScalar& operator*() noexcept + { + return m_scalar; + } + +private: + JsonScalar m_scalar; +}; + +} +#endif diff --git a/src/jrd/json/classes/JsonTypes.cpp b/src/jrd/json/classes/JsonTypes.cpp new file mode 100644 index 00000000000..ae0d1f69c6a --- /dev/null +++ b/src/jrd/json/classes/JsonTypes.cpp @@ -0,0 +1,462 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonTypes.cpp + * DESCRIPTION: Common types used in JSON code. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2024 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "JsonTypes.h" + +#include "../dsql/chars.h" +#include + +#ifdef HAVE_FLOAT_H +#include +#else +static inline constexpr DBL_MAX_10_EXP = 308 +#endif + +#include "unicode/utf16.h" // U16_IS_TRAIL + +using namespace FBJSON; + +static constexpr FB_UINT64 LIMIT_BY_10 = MAX_SINT64 / 10; +static constexpr USHORT UNICODE_HEX_SEQUENCE_LENGTH = 4; // XXXXX +static constexpr USHORT UNICODE_ESCAPED_SEQUENCE_LENGTH = 6; // \uXXXX - with the escape +static constexpr int LEAD_SURROGATE_NULL = -1; + +static inline bool addToValue(SINT64& value, const char c, const bool hasMinus) +{ + if ((static_cast(value) > LIMIT_BY_10) || (value == LIMIT_BY_10 && c > '7')) + { + if (!hasMinus || value != LIMIT_BY_10 || c != '8') + return false; + } + + value = value * 10 + (c - '0'); + return true; +} + +// Exceptions + +json_skippable_exception::json_skippable_exception(const ISC_STATUS *status_vector) throw() + : status_exception(status_vector) +{ } + +json_skippable_exception::json_skippable_exception(const json_skippable_exception& rhs) + : status_exception(rhs) +{ } + +void json_skippable_exception::raise(const JsonStatusVector& statusVector) +{ + throw json_skippable_exception(statusVector.value()); +} + + +json_fatal_exception::json_fatal_exception(const ISC_STATUS *status_vector) throw() + : status_exception(status_vector) +{ } + +json_fatal_exception::json_fatal_exception(const json_fatal_exception& rhs) + : status_exception(rhs) +{ } + + +void json_fatal_exception::raise(const JsonStatusVector& statusVector) +{ + throw json_fatal_exception(statusVector.value()); +} + +// Classes + +template +void FBJSON::JsonScalarParser::parseQuotedString(TView& input, TextPos& current, SmallString& escapelessOutput) +{ + escapelessOutput.clear(); + escapelessOutput.reserve(BUFFER_SMALL); + + const TextPos end = input.end(); + + // UTF-16, two \uXXXX in a row + int leadSurrogate = LEAD_SURROGATE_NULL; + + while (current < end) + { + char c = input[current++]; + + if (c == '"') + { + return; + } + else if ((unsigned char)c < 32) + { + escapelessOutput.printf("%02x", (unsigned char)c); + initError() << JsonStatusMsg(isc_jparser_unescaped_character) << escapelessOutput; + + escapelessOutput.clear(); + return; + } + else if (c == '\\') + { + if (current == end) + { + initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string); + + escapelessOutput.clear(); + return; + } + + c = input[current++]; + + if (c == 'u') + { + // Unicode escape sequence \uXXXX + + if (current + UNICODE_HEX_SEQUENCE_LENGTH >= end) + { + initError() << JsonStatusMsg(isc_jparser_invalid_sequence) << input.getErrorSubstring(current, end - current); + current = end; + + escapelessOutput.clear(); + return; + } + + // Get a digital code of the handling hex sequence to store it to hexToUnicodeValue + int hexToUnicodeValue = 0; + for (USHORT i = 0; i < UNICODE_HEX_SEQUENCE_LENGTH; ++i) + { + c = input[current++]; + const auto cherMeta = classes_array[static_cast(c)]; + if (cherMeta & CHR_DIGIT) + { + hexToUnicodeValue = (hexToUnicodeValue << 4) + (c - '0'); + } + else + { + c = UPPER(c); + if (cherMeta & CHR_HEX) + { + hexToUnicodeValue = (hexToUnicodeValue << 4) + (c - 'A') + 10; + } + else + { + initError() << JsonStatusMsg(isc_jparser_un_incorrect_code) << + input.getErrorSubstring(current - i - 1, UNICODE_HEX_SEQUENCE_LENGTH); + return; + } + } + } + + // The first (lead) surrogate is a 16-bit code value in the range U+D800 to U+DBFF + if (U16_IS_LEAD(hexToUnicodeValue)) // or U_IS_LEAD + { + if (leadSurrogate != LEAD_SURROGATE_NULL) + { + // Two lead codes in a row + initError() << JsonStatusMsg(isc_jparser_un_double_high_surrogate) << + input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH); + return; + } + leadSurrogate = hexToUnicodeValue; + continue; + } + // The second (tail) surrogate is a 16-bit code value in the range U+DC00 to U+DFFF + else if (U16_IS_TRAIL(hexToUnicodeValue)) // or U_IS_TRAIL + { + if (leadSurrogate == LEAD_SURROGATE_NULL) + { + initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) << + input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH); + return; + } + hexToUnicodeValue = U16_GET_SUPPLEMENTARY(leadSurrogate, hexToUnicodeValue); + } + else if (leadSurrogate != LEAD_SURROGATE_NULL) + { + initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) << + input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH); + return; + } + + if (hexToUnicodeValue == 0) + { + initError() << JsonStatusMsg(isc_jparser_un_conv_error); + return; + } + // C0 Controls and Basic Latin Range: 0000–007F (127) + else if (hexToUnicodeValue <= CHAR_MAX) + { + escapelessOutput.append(1, (char)hexToUnicodeValue); + } + else + { + // Append the raw code + + // We are standing to the last hex digit so start and end with offset of 1 + const TextPos unicodeEndPos = current; + + // We need to get the full code sequence including the escape + if (leadSurrogate != LEAD_SURROGATE_NULL) + { + for (TextPos i = unicodeEndPos - (2 * UNICODE_ESCAPED_SEQUENCE_LENGTH); i < unicodeEndPos; ++i) + { + escapelessOutput.append(1, input[i]); + } + + // Two codes, 12 chars - \uXXXX\uXXXX + leadSurrogate = LEAD_SURROGATE_NULL; + } + else // 6 chars - \uXXXX + { + for (TextPos i = unicodeEndPos - UNICODE_ESCAPED_SEQUENCE_LENGTH; i < unicodeEndPos; ++i) + { + escapelessOutput.append(1, input[i]); + } + } + } + + continue; // End of unicode sequence handling + } + + switch (c) + { + case '\'': + case '"': + case '\\': + case '/': + escapelessOutput.append(1, c); + break; + case 'b': + escapelessOutput.append(1, '\b'); + break; + case 'f': + escapelessOutput.append(1, '\f'); + break; + case 'n': + escapelessOutput.append(1, '\n'); + break; + case 'r': + escapelessOutput.append(1, '\r'); + break; + case 't': + escapelessOutput.append(1, '\t'); + break; + default: + escapelessOutput = c; + initError() << JsonStatusMsg(isc_jparser_invalid_sequence) << JsonStatusMsgStrArg(escapelessOutput); + return; + } // !switch + } + else + { + // Handle a simple character + escapelessOutput.append(1, c); + } + + // The unicode tail is missing + // Handle a non-unicode escape character + if (leadSurrogate != LEAD_SURROGATE_NULL) + { + initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) << + input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH); + return; + } + + } // for + + if (current >= end) + { + initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string); + } +} + + +template +ParsedNumber FBJSON::JsonScalarParser::parseNumber(TView& input, TextPos& current, bool strictMode) +{ + const TextPos start = current; + const TextPos end = input.end(); + + ParsedNumber lex; + lex.value = 0; + + if (strictMode) + { + if (current == end) + { + initError() << JsonStatusMsg(isc_jparser_empty_input); + return lex; + } + } + + // Skip spaces, catch first sign + bool hasUnaryMinus = false; + for (; current < end; ++current) + { + const char c = input[current]; + switch (c) + { + case ' ': + continue; + case '-': + ++current; + hasUnaryMinus = true; + break; + case '+': + default: + break; + } + + break; + } + + // Delay overflow error print to lex the full number range and put it into error + bool hasOverflow = false; + + // First part + for (; current < end; ++current) + { + const char c = input[current]; + + if (!(classes(c) & CHR_DIGIT)) + break; + + if (!addToValue(lex.value, c, hasUnaryMinus)) + { + hasOverflow = true; + } + } + + // Decimal part + if (current < end && input[current] == '.') + { + ++current; + lex.isDouble = true; + for (; current < end; ++current) + { + const char c = input[current]; + if (!(classes(c) & CHR_DIGIT)) + break; + + if (!addToValue(lex.value, c, hasUnaryMinus)) + { + hasOverflow = true; + } + + --lex.scale; + } + } + + if (lex.scale < MIN_SCHAR || lex.scale > MAX_SCHAR) + { + hasOverflow = true; + } + + // Exponent sign + SLONG expValue = 0; + if (current < end && (input[current] == 'e' || input[current] == 'E')) + { + lex.isDouble = true; + + if (++current < end) + { + const char next = input[current]; + + bool haveExponentSign = false; + if (next == '+') + { + ++current; + haveExponentSign = false; + } + else if (next == '-') + { + ++current; + haveExponentSign = true; + } + + for (; current < end; ++current) + { + const char c = input[current]; + if (!(classes(c) & CHR_DIGIT)) + break; + + expValue = expValue * 10 + (c - '0'); + } + + if (expValue > DBL_MAX_10_EXP) + { + hasOverflow = true; + } + + if (haveExponentSign) + { + expValue = -expValue; + } + } + + const double maxNum = DBL_MAX / static_cast(std::pow(10, expValue)); + if (double(lex.value) > maxNum) + { + hasOverflow = true; + } + + lex.scale += static_cast(expValue); + } + + if (hasOverflow) + { + initError() << JsonStatusMsg(isc_jparser_number_overflow) << + JsonStatusMsgStrArg(input.getErrorSubstring(start, end - start)); + + return lex; + } + + if (hasUnaryMinus) + lex.value = -lex.value; + + if (strictMode) + { + for (; current < end; ++current) + { + const char c = input[current]; + + if (!(classes(c) & CHR_WHITE)) + { + initError() << JsonStatusMsg(isc_jparser_invalid_number_parse) << + JsonStatusMsgStrArg(input.getErrorSubstring(start)); + break; + } + } + } + + return lex; +} + +template +JsonStatusMsg& JsonScalarParser::initError() +{ + error.reset(FB_NEW JsonStatusMsg()); + return *error.get(); +} + +template class FBJSON::JsonScalarParser; +// template class FBJSON::JsonScalarParser; diff --git a/src/jrd/json/classes/JsonTypes.h b/src/jrd/json/classes/JsonTypes.h new file mode 100644 index 00000000000..e9cb096d656 --- /dev/null +++ b/src/jrd/json/classes/JsonTypes.h @@ -0,0 +1,312 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonTypes.h + * DESCRIPTION: Common types used in JSON code. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2024 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_TYPES_H +#define JSON_TYPES_H + +#include "firebird.h" +#include "fb_exception.h" +#include "../JsonConsts.h" + +namespace FBJSON { + +using ArraySize = ULONG; +using RangeSize = SSHORT; + +// JsonPath Extention: Negative values supported +using PathArrayIndex = SLONG; + +// Enums + +// A sub-enum for JsonType and JsonbType enums +// The order represents compare cardinality: +// null < SQL/JSON scalar < array < object +enum CommonJsonType : UCHAR +{ + JT_EMPTY = 0, // Special type + JT_NULL = 1, + JT_SCALAR = 2, + JT_ARRAY = 3, + JT_OBJECT = 4 +}; + +enum class JsonType : UCHAR +{ + Empty = JT_EMPTY, // Special type + Null = JT_NULL, + Scalar = JT_SCALAR, + Array = JT_ARRAY, + Object = JT_OBJECT +}; + +enum class JsonbType : UCHAR +{ + Empty = JT_EMPTY, + Scalar = JT_SCALAR, // Scalar or null + Array = JT_ARRAY, + Object = JT_OBJECT, + DuplicateKey +}; + +enum class PathMethod +{ + NONE = 0, + TYPE = 1, + SIZE = 2, + DOUBLE = 3, + CEILING = 4, + FLOOR = 5, + ABS = 6, + DATETIME = 7, + KEYVALUE = 8, + + // Pass unary operations as methods in dynamic path to simplify things + // In a math expr and filter, normal dynamic operators are used + METHOD_UNARY_PLUS, + METHOD_UNARY_MINUS, + + // SQL 2023 methods + + STRING, + BOOLEAN, + BIGINT, + DECIMAL, + INTEGER, + NUMBER, + + DATE, + TIME, + TIME_TZ, + TIMESTAMP, + TIMESTAMP_TZ +}; + +enum class PathType +{ + MAIN = 0, //$ + MAIN_IN_FILTER, //$ + ITEM, //@ + PASSING //$ +}; + +// Element of JSON Path +enum class ItemType : UCHAR +{ + FIELD, // $.field + ARRAY_ELEMENT // $[0] +}; + +// Helpers + +constexpr void rtrim(std::string_view& view) +{ + size_t stringLength = view.length(); + while (stringLength > 0 && view[stringLength - 1] == ' ') + { + --stringLength; + } + view.remove_suffix(view.length() - stringLength); +} + +// Common types + +class json_skippable_exception : public Firebird::status_exception +{ +public: + explicit json_skippable_exception(const ISC_STATUS *status_vector) throw(); + + json_skippable_exception(const json_skippable_exception& rhs); + json_skippable_exception(json_skippable_exception&&) noexcept = delete; + + json_skippable_exception& operator=(const json_skippable_exception&) = delete; + json_skippable_exception& operator=(json_skippable_exception&&) noexcept = delete; + + virtual ~json_skippable_exception() = default; + + [[noreturn]] static void raise(const JsonStatusVector& statusVector); +}; + +class json_fatal_exception : public Firebird::status_exception +{ +public: + explicit json_fatal_exception(const ISC_STATUS *status_vector) throw(); + + json_fatal_exception(const json_fatal_exception& rhs); + json_fatal_exception(json_fatal_exception&&) noexcept = delete; + + json_fatal_exception& operator=(const json_fatal_exception&) = delete; + json_fatal_exception& operator=(json_fatal_exception&&) noexcept = delete; + + virtual ~json_fatal_exception() = default; + + [[noreturn]] static void raise(const JsonStatusVector& statusVector); +}; + +using json_syntax_exception = json_fatal_exception; +using json_strict_exception = json_fatal_exception; + +struct JsonLevelNode +{ + JsonLevelNode* next = nullptr; + JsonLevelNode* prev = nullptr; + + SmallString field; + ArraySize indexInArray = 0; + ArraySize arraySize = 0; + SSHORT depth = 0; + CommonJsonType type = JT_EMPTY; + ItemType itemType = ItemType::FIELD; + + JsonLevelNode(MemoryPool& pool) : + field(pool) + { } + + JsonLevelNode(const JsonLevelNode&) = delete; + JsonLevelNode(JsonLevelNode&&) noexcept = delete; + + JsonLevelNode& operator=(const JsonLevelNode&) = delete; + JsonLevelNode& operator=(JsonLevelNode&&) noexcept = delete; + + ~JsonLevelNode() + { + JsonLevelNode* current = next; + JsonLevelNode* curNext; + while (current) + { + curNext = current->next; + current->next = nullptr; + delete current; + current = curNext; + } + } + + inline ArraySize getArraySize() const noexcept + { + // The current level is an element; jsonNode->prev is an array level + if (prev && prev->isArray()) + return prev->arraySize; + else + return 1; + } + + inline bool isArray() const noexcept + { + return type == JT_ARRAY; + } +}; + +struct ParsedNumber +{ + SINT64 value = 0; + SLONG scale = 0; + bool isDouble = false; + + inline double getDouble() const + { + static constexpr double scaleMove = 10.0; + + double result = static_cast(value); + if (scale > 0) + { + for (USHORT i = 0; i < scale; ++i) + result *= scaleMove; + } + else + { + for (USHORT i = 0; i < abs(scale); ++i) + result /= scaleMove; + } + + return result; + } +}; + + +template +class JsonScalarParser +{ +public: + // JsonStatusMsg allocation is super expansive + Firebird::AutoPtr error; + + bool hasError() const + { + return error && error->isDirty(); + } + + void parseQuotedString(TView& view, TextPos& current, SmallString& escapelessOutput); + + // strictMode for unary plus/minus and parse errors + ParsedNumber parseNumber(TView& input, TextPos& current, const bool strictMode); + +private: + JsonStatusMsg& initError(); +}; + + +struct StringParseView +{ + std::string_view base; + char operator[](TextPos i) + { + return base[i]; + } + + SmallString getErrorSubstring(TextPos subStart, TextPos subLength = JSON_MAX_REPORT_SIZE) const + { + if (subStart + subLength > base.length()) + { + subLength = base.length() - subStart; + } + + bool overload = false; + if (subLength > JSON_MAX_REPORT_SIZE) + { + overload = true; + subLength = JSON_MAX_REPORT_SIZE; + } + + SmallString buffer(base.data() + subStart, subLength); + if (overload) + { + buffer += "..."; + return buffer; + } + else + return buffer; + } + + inline TextPos end() const + { + return static_cast(base.length()); + } +}; + +using StringParser = JsonScalarParser; +// using InputJsonParser = JsonScalarParser; + +} +#endif diff --git a/src/jrd/json/path/JPathParser.cpp b/src/jrd/json/path/JPathParser.cpp new file mode 100644 index 00000000000..4a5c77d9242 --- /dev/null +++ b/src/jrd/json/path/JPathParser.cpp @@ -0,0 +1,615 @@ + +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2023 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" +#include +#include +#include "JPathParser.h" +#include "../JsonUtils.h" +#include "../dsql/chars.h" +#include "../jrd/DataTypeUtil.h" +#include "../jrd/intl_proto.h" + +using namespace Firebird; +using namespace FBJSON; + + +class PathTokens +{ +public: + LeftPooledMap keyword; + LeftPooledMap methods; + PathTokens(MemoryPool& p) : + keyword(p), methods(p) + { + keyword.put("lax", TOK_LAX); + keyword.put("strict", TOK_STRICT); + keyword.put("last", TOK_LAST); + keyword.put("to", TOK_TO); + + keyword.put("null", TOK_JSON_NULL); + keyword.put("true", TOK_TRUE); + keyword.put("false", TOK_FALSE); + + keyword.put("starts", TOK_STARTS); + keyword.put("with", TOK_WITH); + keyword.put("like_regex", TOK_LIKE_REGEX); + keyword.put("is", TOK_IS); + keyword.put("unknown", TOK_UNKNOWN); + keyword.put("flags", TOK_FLAGS); + keyword.put("exists", TOK_EXISTS); + + // Process the follows methods in a special way + keyword.put("keyvalue", TOK_KEYVALUE); + keyword.put("datetime", TOK_DATETIME); + keyword.put("time", TOK_TIME); + keyword.put("time_tz", TOK_TIME_TZ); + keyword.put("timestamp", TOK_TIMESTAMP); + keyword.put("timestamp_tz", TOK_TIMESTAMP_TZ); + keyword.put("decimal", TOK_DECIMAL); + + // Methods without an arg + methods.put("type", PathMethod::TYPE); + methods.put("size", PathMethod::SIZE); + methods.put("double", PathMethod::DOUBLE); + methods.put("ceiling", PathMethod::CEILING); + methods.put("floor", PathMethod::FLOOR); + methods.put("abs", PathMethod::ABS); + + methods.put("date", PathMethod::DATE); + methods.put("string", PathMethod::STRING); + methods.put("boolean", PathMethod::BOOLEAN); + methods.put("bigint", PathMethod::BIGINT); + methods.put("integer", PathMethod::INTEGER); + methods.put("number", PathMethod::NUMBER); + } +}; + +GlobalPtr tokens; + + +const int* getKeyword(const string& str) +{ + return tokens->keyword.get(str); +} + +const int getNonCharKeyword(const char* str) +{ + switch (getSuperType(str[0], str[1])) + { + case getSuperType('|', '|'): + return TOK_OR; + case getSuperType('&', '&'): + return TOK_AND; + case getSuperType('=', '='): + return TOK_EQ; + case getSuperType('!', '='): + case getSuperType('<', '>'): + return TOK_NEQ; + case getSuperType('<', '='): + return TOK_LEQ; + case getSuperType('>', '='): + return TOK_GEQ; + } + + return 0; +} + + +PathParser::PathParser(MemoryPool& pool, const std::string_view jsonPath) : + PermanentStorage(pool), + m_tempPool(MemoryPool::createPool()) +{ + yyps = 0; + yypath = 0; + yylvals = 0; + yylvp = 0; + yylve = 0; + yylvlim = 0; + yylpsns = 0; + yylpp = 0; + yylpe = 0; + yylplim = 0; + yylexp = 0; + yylexemes = 0; + + const char* begin = jsonPath.data(); + const char* end = begin + jsonPath.length(); + + yyposn.firstLine = 1; + yyposn.firstColumn = 1; + yyposn.lastLine = 1; + yyposn.lastColumn = 1; + yyposn.firstPos = begin; + yyposn.leadingFirstPos = begin; + yyposn.lastPos = end; + yyposn.trailingLastPos = end; + + lex.start = begin; + lex.line_start = lex.last_token = lex.ptr = lex.leadingPtr = begin; + lex.end = end; + lex.line_start_bk = lex.line_start; + lex.prev_keyword = -1; + +#ifdef DSQL_DEBUG + if (DSQL_debug & 32) + dsql_trace("Source DSQL string:\n%.*s", (int) length, string); +#endif +} + + +PathParser::~PathParser() +{ + while (yyps) + { + yyparsestate* p = yyps; + yyps = p->save; + yyFreeState(p); + } + + while (yypath) + { + yyparsestate* p = yypath; + yypath = p->save; + yyFreeState(p); + } + + delete[] yylvals; + delete[] yylpsns; + delete[] yylexemes; +} + + +JsonPathExpr* PathParser::parse(const PassingKeys* keys) +{ + this->m_passingKeys = keys; + + m_output.reset(FB_NEW_POOL(getPool()) JsonPathExpr(getPool())); + + if (parseAux() != 0) + { + fb_assert(false); + } + + if (!m_allowPathExtensions && m_output->hasMath()) + { + json_syntax_exception::raise( + JsonStatusMsg(isc_jpath_common) << + JsonStatusMsg(isc_jpath_multiple_roots)); + } + + return m_output.release(); +} + +// Set the position of a left-hand non-terminal based on its right-hand rules. +void PathParser::yyReducePosn(YYPOSN& ret, YYPOSN* termPosns, YYSTYPE* /*termVals*/, int termNo, + int /*stkPos*/, int /*yychar*/, YYPOSN& /*yyposn*/, void*) +{ + if (termNo == 0) + { + // Accessing termPosns[-1] seems to be the only way to get correct positions in this case. + ret.firstLine = ret.lastLine = termPosns[termNo - 1].lastLine; + ret.firstColumn = ret.lastColumn = termPosns[termNo - 1].lastColumn; + ret.firstPos = ret.lastPos = ret.trailingLastPos = termPosns[termNo - 1].trailingLastPos; + ret.leadingFirstPos = termPosns[termNo - 1].lastPos; + } + else + { + ret.firstLine = termPosns[0].firstLine; + ret.firstColumn = termPosns[0].firstColumn; + ret.firstPos = termPosns[0].firstPos; + ret.leadingFirstPos = termPosns[0].leadingFirstPos; + ret.lastLine = termPosns[termNo - 1].lastLine; + ret.lastColumn = termPosns[termNo - 1].lastColumn; + ret.lastPos = termPosns[termNo - 1].lastPos; + ret.trailingLastPos = termPosns[termNo - 1].trailingLastPos; + } +} + + +int PathParser::yylex() +{ + if (!yylexSkipSpaces()) + return -1; + + yyposn.firstLine = 1; + yyposn.firstColumn = lex.ptr - lex.line_start; + yyposn.firstPos = lex.ptr - 1; + yyposn.leadingFirstPos = lex.leadingPtr; + + lex.prev_keyword = yylexAux(); + + yyposn.lastPos = lex.ptr; + lex.leadingPtr = lex.ptr; + + bool spacesSkipped = yylexSkipSpaces(); + + yyposn.lastLine = 1; + yyposn.lastColumn = lex.ptr - lex.line_start; + + if (spacesSkipped) + --lex.ptr; + + yyposn.trailingLastPos = lex.ptr; + + return lex.prev_keyword; +} + + +bool PathParser::yylexSkipSpaces() +{ + UCHAR tok_class; + SSHORT c; + + // Find end of white space + for (;;) + { + if (lex.ptr >= lex.end) + return false; + + if (yylexSkipEol()) + continue; + + c = *lex.ptr++; + tok_class = classes(c); + + if (!(tok_class & CHR_WHITE)) + break; + } + + return true; +} + + +bool PathParser::yylexSkipEol() +{ + bool eol = false; + const TEXT c = *lex.ptr; + + if (c == '\r') + { + lex.ptr++; + if (lex.ptr < lex.end && *lex.ptr == '\n') + lex.ptr++; + + eol = true; + } + else if (c == '\n') + { + lex.ptr++; + eol = true; + } + + if (eol) + { + lex.line_start = lex.ptr; // + 1; // CVC: +1 left out. + } + + return eol; +} + +static inline bool isTokenChar(const char c) +{ + return (classes(c) & CHR_LETTER) || c == '_'; +} + +static inline bool isStringChar(const char c) +{ + return classes(c) & CHR_IDENT; +} + +static inline const char* skipSpaces(const char* start, const char* end) +{ + const char* it = start; + for (; it < end && isStringChar(*it); ++it); + + return it; +} + +static inline const char* readToken(const char* start, const char* end) +{ + const char* it = start; + for (; it < end && isTokenChar(*it); ++it); + + return it; +} + +static inline const char* readString(const char* start, const char* end) +{ + const char* it = start; + for (; it < end && isStringChar(*it); ++it); + + return it; +} + +static inline const char* readToken(const char* start, const char* end, const char stop) +{ + const char* it = start; + for (; it < end && isTokenChar(*it) && *it != stop; ++it); + + return it; +} + +int PathParser::yylexAux() +{ + SSHORT c = lex.ptr[-1]; + UCHAR tok_class = classes(c); + lex.last_token = lex.ptr - 1; + + // It does not work for lex errors to set the flag in the yacc file + switch (lex.prev_keyword) + { + case static_cast('?'): + m_lexInFilter = true; + m_filterBracketCounter = 0; + break; + case static_cast('('): + ++m_filterBracketCounter; + break; + case static_cast(')'): + if (m_lexInFilter && --m_filterBracketCounter == 0) + m_lexInFilter = false; + break; + } + + + // Parse a quoted string, being sure to look for double quotes + if (tok_class & CHR_QUOTE) + { + StringParser scalarParser; + TextPos offset = 0; + std::string_view view(lex.ptr, lex.end - lex.ptr); + StringParseView viewp{view}; + string* stringPtr = mkTempString(""); + scalarParser.parseQuotedString(viewp, offset, *stringPtr); + + lex.ptr += offset; + if (scalarParser.hasError()) + { + raiseFullErrorVector(*scalarParser.error, yyposn); + } + + yylval.stringPtr = stringPtr; + return TOK_QUOTED_STRING; + } + + fb_assert(lex.ptr <= lex.end); + + if ((tok_class & CHR_DIGIT) || + ((c == '.') && (lex.ptr < lex.end) && (classes(*lex.ptr) & CHR_DIGIT))) + { + --lex.ptr; + + StringParser scalarParser; + TextPos offset = 0; + std::string_view view(lex.ptr, lex.end - lex.ptr); + StringParseView viewp{view}; + const ParsedNumber number = scalarParser.parseNumber(viewp, offset, false); + + lex.ptr += offset; + if (scalarParser.hasError()) + { + raiseFullErrorVector(*scalarParser.error, yyposn); + } + + // Should we use floating point type? + if (number.isDouble) + { + lex.last_token_bk = lex.last_token; + lex.line_start_bk = lex.line_start; + + yylval.doubleVal = number.getDouble(); + return TOK_DOUBLE; + } + else + { + lex.last_token_bk = lex.last_token; + lex.line_start_bk = lex.line_start; + + if (number.value <= MAX_SSHORT) + { + yylval.int16Val = (SSHORT) number.value; + return TOK_INTEGER16; + } + else if (number.value <= MAX_SLONG) + { + yylval.int32Val = (SLONG) number.value; + return TOK_INTEGER32; + } + else + { + yylval.int64Val = number.value; + return TOK_INTEGER64; + } + } + } + + // Restore the status quo ante, before we started our unsuccessful + // attempt to recognize a number. + lex.ptr = lex.last_token; + c = *lex.ptr++; + // We never touched tok_class, so it doesn't need to be restored. + + if (c == '$' && lex.last_token + 1 < lex.end && isStringChar(*lex.ptr) && *lex.ptr != '$') + { + const char* start = lex.ptr - 1; + lex.ptr = readString(lex.ptr, lex.end); + + const string rawPassingStr(start + 1, lex.ptr - start - 1); // Skip $ + yylval.stringPtr = mkTempString(rawPassingStr); + return TOK_PASSING_NAME; + } + else if (c == '.') + { + const char* methodStart = skipSpaces(lex.ptr, lex.end); + const char* end = readToken(methodStart, lex.end, '('); + if (*end == '(') + { + return TOK_METHOD_DOT; + } + } + else if (tok_class & CHR_LETTER) + { + // An quoteless filed name or a token or a passing variable + + // First get only token chars + const char* start = lex.ptr - 1; + lex.ptr = readToken(lex.ptr, lex.end); + + string rawStr(start, lex.ptr - start); + const int* keyVer = getKeyword(rawStr); + if (keyVer) + { + lex.last_token_bk = lex.last_token; + lex.line_start_bk = lex.line_start; + return *keyVer; + } + else + { + FBJSON::PathMethod* method = tokens->methods.get(rawStr); + if (method) + { + const TEXT* src = lex.ptr; + for (; lex.ptr < lex.end && isspace(*lex.ptr); lex.ptr++) + { + if (lex.ptr >= lex.end) + return -1; + } + if (*lex.ptr == '(') + { + yylval.methodType = *method; + lex.last_token_bk = lex.last_token; + lex.line_start_bk = lex.line_start; + return TOK_METHOD; + } + else + lex.ptr = src; + } + } + + // Get the rest on allowed tokens for a string + lex.ptr = skipSpaces(lex.ptr, lex.end); + rawStr.assign(start, lex.ptr - start); + + + yylval.stringPtr = mkTempString(rawStr); + lex.last_token_bk = lex.last_token; + lex.line_start_bk = lex.line_start; + return TOK_STRING; + } + + // Must be punctuation -- test for double character punctuation + if (lex.last_token + 1 < lex.end && !isspace(UCHAR(lex.last_token[1]))) + { + const int keyVer = getNonCharKeyword(lex.last_token); + if (keyVer != 0) + { + ++lex.ptr; + return keyVer; + } + } + + // Single character punctuation are simply passed on + return (UCHAR) c; +} + + +void PathParser::raiseFullErrorVector(const JsonStatusMsg& error, const YYPOSN& posn) +{ + string token(posn.firstPos); + if (token.length() > JSON_MAX_REPORT_SIZE) + { + token.resize(JSON_MAX_REPORT_SIZE); + token += "..."; + } + + if (posn.firstLine == 1) + { + json_syntax_exception::raise( + JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) << + error << + JsonStatusMsg(isc_jpath_problem_place) << + JsonStatusMsgIntArg(FB_UINT64(posn.firstColumn)) << + JsonStatusMsgStrArg(token)); + } + else + { + json_syntax_exception::raise( + JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) << + error << + JsonStatusMsgIntArg(FB_UINT64((posn.firstColumn))) << + JsonStatusMsgIntArg(FB_UINT64((posn.firstLine))) << + JsonStatusMsgStrArg(token)); + } +} + +void PathParser::yyerror_detailed(const TEXT* /*error_string*/, int yychar, YYSTYPE&, YYPOSN& posn) +{ +/************************************** + * + * y y e r r o r _ d e t a i l e d + * + ************************************** + * + * Functional description + * Print a syntax error. + * + **************************************/ + + if (yychar < 1) + { + if (posn.firstLine == 1) + { + json_syntax_exception::raise(JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) << + JsonStatusMsg(isc_jlexer_end) << + JsonStatusMsgIntArg(FB_UINT64(posn.firstColumn))); + } + else + { + json_syntax_exception::raise(JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) << + JsonStatusMsg(isc_jlexer_end_with_line) << + JsonStatusMsgIntArg(FB_UINT64(posn.firstColumn)) << + JsonStatusMsgIntArg(FB_UINT64(posn.firstLine))); + } + } + else + { + raiseFullErrorVector(JsonStatusMsg(isc_jlexer_invalid_syntax), posn); + } +} + + +void PathParser::checkPassing(const SmallString& passName) const +{ + if (m_passingKeys == nullptr) + { + json_syntax_exception::raise(JsonStatusMsg(isc_jpath_common) << + JsonStatusMsg(isc_jlexer_passing_not_defined)); + } + else if (!m_passingKeys->exist(passName)) + { + json_syntax_exception::raise(JsonStatusMsg(isc_jpath_common) << + JsonStatusMsg(isc_jlexer_passing_var_not_defined) << + JsonStatusMsgStrArg(passName)); + } +} diff --git a/src/jrd/json/path/JPathParser.h b/src/jrd/json/path/JPathParser.h new file mode 100644 index 00000000000..e2d177ac9be --- /dev/null +++ b/src/jrd/json/path/JPathParser.h @@ -0,0 +1,498 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2023 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_PARSER_H +#define JSON_PARSER_H + +#include "firebird.h" +#include "../common/classes/fb_string.h" +#include "../common/classes/PodOptional.h" + +#include "../jrd/json/JsonRuntime.h" +#include "../jrd/json/path/JsonPath.h" + +namespace FBJSON { + +// Define before jparse.h +struct JsonPathContext +{ + FBJSON::JsonPath* main = nullptr; + PathNode* current = nullptr; + + PathVariable* variable = nullptr; + bool dropMain = false; + + JsonPathContext() + { } + + JsonPathContext(JsonPathContext&&) noexcept = delete; + JsonPathContext(const JsonPathContext&) = delete; + + JsonPathContext& operator=(JsonPathContext&&) noexcept = delete; + JsonPathContext& operator=(const JsonPathContext&) = delete; + + ~JsonPathContext() + { + if (dropMain) + delete main; + } + + inline void addRange(const RangeSize up) + { + current->ranges.add().init(up); + } + + inline void addRange(const RangeSize up, const RangeSize bot) + { + current->ranges.add().init(up, bot); + } + + void clearRange() + { + current->ranges.clear(); + } + + inline JsonPath* extract() noexcept + { + dropMain = false; + return main; + } + + inline bool isEmpty() const + { + return main->isZeroPath() && !current->filterNode.hasData(); + } +}; + +} // namespace + +#include "../gen/jparse.h" + +using PassingKeys = Firebird::LeftPooledMap; + +namespace FBJSON { + + +class PathParser : public Firebird::PermanentStorage +{ +public: + PathParser(MemoryPool& pool, const std::string_view jsonPath); + + PathParser(PathParser&&) noexcept = delete; + PathParser(const PathParser&) = delete; + + PathParser& operator=(PathParser&&) noexcept = delete; + PathParser& operator=(const PathParser&) = delete; + + ~PathParser(); + + JsonPathExpr* parse(const PassingKeys* keys = nullptr); + + // Allow only one root expression for functions like JSON_EXISTS + inline void setMultipleRoots(const bool allow) noexcept + { + m_allowPathExtensions = allow; + } + +private: + // User-defined text position type. + struct Position + { + ULONG firstLine; + ULONG firstColumn; + ULONG lastLine; + ULONG lastColumn; + const char* firstPos; + const char* lastPos; + const char* leadingFirstPos; + const char* trailingLastPos; + }; + + typedef Position YYPOSN; + typedef int Yshort; + + struct yyparsestate + { + yyparsestate* save; // Previously saved parser state + int state; + int errflag; + Yshort* ssp; // state stack pointer + YYSTYPE* vsp; // value stack pointer + YYPOSN* psp; // position stack pointer + YYSTYPE val; // value as returned by actions + YYPOSN pos; // position as returned by universal action + Yshort* ss; // state stack base + YYSTYPE* vs; // values stack base + YYPOSN* ps; // position stack base + int lexeme; // index of the conflict lexeme in the lexical queue + unsigned int stacksize; // current maximum stack size + Yshort ctry; // index in yyctable[] for this conflict + }; + + struct LexerState + { + // This is, in fact, parser state. Not used in lexer itself + int dsql_debug; + + // Actual lexer state begins from here + + const TEXT* leadingPtr; + const TEXT* ptr; + const TEXT* end; + const TEXT* last_token; + const TEXT* start; + const TEXT* line_start; + const TEXT* last_token_bk; + const TEXT* line_start_bk; + int prev_keyword; + }; + + [[maybe_unused]] + int yydebug = 0; + + JsonPathContext* createContext() + { + JsonPathContext* context = FB_NEW_POOL(*m_tempPool) JsonPathContext(); + context->main = FB_NEW_POOL(getPool()) JsonPath(getPool()); + context->dropMain = true; + context->current = context->main->getRootNode(); + + return context; + } + + JsonPathContext* createMainContext() + { + if (m_mainRootCreated) + { + if (!m_parseInFilter && !m_allowPathExtensions) + { + json_syntax_exception::raise( + JsonStatusMsg(isc_jpath_common) << + JsonStatusMsg(isc_jpath_multiple_roots)); + } + + return createVariableContext(PathVariable::Type::JSON); + } + else + { + m_mainRootCreated = true; + + // Main path + JsonPathContext* context = FB_NEW_POOL(*m_tempPool) JsonPathContext(); + context->dropMain = false; + context->main = m_output->getJsonPath(); + context->current = context->main->getRootNode(); + context->variable = FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::ROOT); + + return context; + } + } + + JsonPathContext* createVariableContext(const PathVariable::Type type) + { + if (!m_parseInFilter) + { + if (type == PathVariable::Type::ITEM) + { + json_syntax_exception::raise( + JsonStatusMsg(isc_jpath_common) << + JsonStatusMsg(isc_jpath_illegal_item_variable)); + } + + if (type == PathVariable::Type::PASSING) + { + m_mainRootCreated = true; // Allow passing instead of root path expression ($.) + } + } + + PathVariable* var = FB_NEW_POOL(getPool()) PathVariable(getPool(), type); + return createVariableContext(*var); + } + + JsonPathContext* createVariableContext(PathVariable& var) + { + JsonPathContext* context = FB_NEW_POOL(*m_tempPool) JsonPathContext(); + var.path.reset(FB_NEW_POOL(getPool()) JsonPath(getPool())); + if (!m_isLaxMode) + var.path->setStrictFlag(); + + context->dropMain = false; + context->main = var.path.get(); + context->current = var.path->getRootNode(); + context->variable = &var; + + return context; + } + + JsonPathContext* createPassingContext(const SmallString& name) + { + checkPassing(name); + JsonPathContext* context = createVariableContext(PathVariable::Type::PASSING); + context->variable->name = name; + return context; + } + + PathInjection& addPathInjection(JsonPathContext* main, const SmallString& name) + { + checkPassing(name); + PathVariable* passing = FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::PASSING); + passing->name = name; + + return addPathInjection(main, FB_NEW_POOL(getPool()) JsonExprNode(getPool(), passing)); + } + + PathInjection& addPathInjection(JsonPathContext* context, JsonExprNode* node) + { + auto& injection = context->main->addInjection(); + injection.expr = node; + injection.depth = context->current->depth + 1; + + return injection; + } + + void unwrapPrimary(JsonExprNode* nodeToUnwrap) + { + if (!nodeToUnwrap->isVariable()) + return; + + if (nodeToUnwrap->getVariable()->type == PathVariable::Type::ROOT) + { + m_output->getJsonPath()->unwrap(); + } + else + nodeToUnwrap->unwrap(); + } + +private: + Firebird::string* mkTempString(const Firebird::string& s) + { + return FB_NEW_POOL(*m_tempPool) Firebird::string(*m_tempPool, s); + } + + Firebird::string* mkTempString(const char* s) + { + return FB_NEW_POOL(*m_tempPool) Firebird::string(*m_tempPool, s); + } + + template + JsonExprNode* mkNode() + { + return JsonExprNode::make(getPool()); + } + + JsonExprNode* mkVariableNode(PathVariable* var) + { + return FB_NEW_POOL(getPool()) JsonExprNode(getPool(), var); + } + + JsonExprNode* mkMethodNode(const PathMethod method, JsonExprNode* arg = nullptr) + { + auto* node = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), method); + + if (arg) + node->addChild(arg); + + return node; + } + + JsonExprNode* addOptTailNode(JsonExprNode* head, JsonExprNode* newTail) + { + if (newTail == nullptr) + return head; + + bool needUnwrap = m_isLaxMode && JsonPath::isUnwrapMethod(newTail->getPathMethod()); + + // Inject head into expression + if (!head->canHasTail()) + { + if (needUnwrap) + unwrapPrimary(head); + + newTail->setHead(head); + return newTail; + } + + // Add new tail + if (head->hasTail()) + { + JsonExprNode* tailEnd = head->getTailNode(); + addOptTailNode(tailEnd, newTail); // tail after tail + return head; + } + + // Always inject root into expression + const bool isRootVariable = head->isVariable() && head->getVariable()->type == PathVariable::Type::ROOT; + if (isRootVariable) + { + if (needUnwrap) + unwrapPrimary(head); + + newTail->setHead(head); + return newTail; + } + + if (needUnwrap && head->testType(JsonExprNode::VARIABLE_NODE)) + { + head->unwrap(); // Unwrap variable + needUnwrap = false; + } + + if (!newTail->testType(JsonExprNode::FILTER_NODE)) + newTail->makeHeadNode(needUnwrap); + + // Add the next method as the second "tail" path + head->addChild(newTail); + + return head; + } + + void setPrimary(JsonExprNode* arithmetic) + { + if (!m_mainRootCreated) + { + json_syntax_exception::raise( + JsonStatusMsg(isc_jpath_common) << + JsonStatusMsg(isc_jpath_missing_root)); + } + + fb_assert(m_output->getTail() == nullptr); + fb_assert(m_output->getMath() == nullptr); + m_output->resetExpr(m_pathRootTail.release(), arithmetic); + } + + JsonExprNode* completePath(JsonExprNode* node) + { + JsonExprNode* rnode = node; + while (!rnode->isVariable()) // While method or a filter - get the source argument + { + if (rnode->getChildrenCount() == 0) + return node; + + rnode = rnode->getFirstChild(); + } + + if (!m_pathRootTail.hasData() && rnode->getVariable()->type == PathVariable::Type::ROOT) + { + fb_assert(!node->testType(JsonExprNode::CALCULATION_NODE)); + m_pathRootTail.reset(node); + + // Return a dummy header node + return mkVariableNode(FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::ROOT)); + } + else + { + return node; + } + } + + void yyReducePosn(YYPOSN& ret, YYPOSN* termPosns, YYSTYPE* termVals, + int termNo, int stkPos, int yychar, YYPOSN& yyposn, void*); + + int yylex(); + bool yylexSkipSpaces(); + bool yylexSkipEol(); // returns true if EOL is detected and skipped + int yylexAux(); + + void yyerror_detailed(const TEXT* error_string, int yychar, YYSTYPE&, YYPOSN&); + +private: + void checkPassing(const SmallString& pass) const; + void raiseFullErrorVector(const JsonStatusMsg& error, const YYPOSN& posn); + + +// start - defined in btyacc_json.ske +private: + static void yySCopy(YYSTYPE* to, YYSTYPE* from, int size); + static void yyPCopy(YYPOSN* to, YYPOSN* from, int size); + static void yyFreeState(yyparsestate* p); + + void yyMoreStack(yyparsestate* yyps); + yyparsestate* yyNewState(int size); + + +private: + int parseAux(); + int yylex1(); + int yyexpand(); +// end - defined in btyacc_json.ske + +private: + USHORT parser_version; + + Firebird::string transformedString; + + // These value/posn are taken from the lexer + YYSTYPE yylval; + YYPOSN yyposn; + + // These value/posn of the root non-terminal are returned to the caller + YYSTYPE yyretlval; + Position yyretposn; + + int yynerrs; + int yym; // ASF: moved from local variable of Parser::parseAux() + + // Current parser state + yyparsestate* yyps; + // yypath!=NULL: do the full parse, starting at *yypath parser state. + yyparsestate* yypath; + // Base of the lexical value queue + YYSTYPE* yylvals; + // Current posistion at lexical value queue + YYSTYPE* yylvp; + // End position of lexical value queue + YYSTYPE* yylve; + // The last allocated position at the lexical value queue + YYSTYPE* yylvlim; + // Base of the lexical position queue + Position* yylpsns; + // Current posistion at lexical position queue + Position* yylpp; + // End position of lexical position queue + Position* yylpe; + // The last allocated position at the lexical position queue + Position* yylplim; + // Current position at lexical token queue + Yshort* yylexp; + Yshort* yylexemes; + + LexerState lex; + +private: + Firebird::AutoMemoryPool m_tempPool; + Firebird::AutoPtr m_output; + Firebird::AutoPtr m_pathRootTail; + + const PassingKeys* m_passingKeys = nullptr; + + SLONG m_filterBracketCounter = 0; + bool m_isLaxMode = true; // Global state + bool m_lexInFilter = false; // For error printing + bool m_parseInFilter = false; // For context check + bool m_mainRootCreated = false; + + bool m_allowPathExtensions = true; +}; + +} + +#endif // DSQL_PARSER_H diff --git a/src/jrd/json/path/JsonPath.cpp b/src/jrd/json/path/JsonPath.cpp new file mode 100644 index 00000000000..dd77e89c84b --- /dev/null +++ b/src/jrd/json/path/JsonPath.cpp @@ -0,0 +1,296 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonPath.cpp + * DESCRIPTION: JSON Path parser. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Denis Logashov + * for Red Soft Corporation. + * + * Copyright (c) 2017 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "JsonPath.h" +#include "../JsonUtils.h" +#include "../JsonConsts.h" +#include "../JsonRuntime.h" + +#include "JPathParser.h" + +using namespace Firebird; +using namespace FBJSON; + +JsonPath::JsonPath(MemoryPool& pool, const bool createPath) + : Firebird::PermanentStorage(pool), + m_injections(pool) +{ + if (createPath) + resetRootNode(); +} + +JsonPath::~JsonPath() +{ } + +PathNode* JsonPath::resetRootNode() +{ + m_pathRootNode.reset(FB_NEW_POOL(getPool()) PathNode(getPool())); + return m_pathRootNode.get(); +} + +PathInjection& JsonPath::addInjection() +{ + return m_injections.add(); +} + +void JsonPath::unwrap() +{ + fb_assert(m_pathRootNode != nullptr); + PathNode* current = m_pathRootNode; + while (current->next) + { + current = current->next; + } + current->flags |= PathNode::FLAG_UNWRAP; +} + +bool JsonPath::isZeroPath() const +{ + fb_assert(m_pathRootNode != nullptr); + return m_pathRootNode->next == nullptr; +} + +bool JsonPath::isWrappingZeroPath() const +{ + fb_assert(m_pathRootNode != nullptr); + return m_pathRootNode->next != nullptr && + m_pathRootNode->next->next == nullptr && + m_pathRootNode->next->matchWrapPattern(); +} + +bool JsonPath::isEmpty(const UCHAR flagsToCheck) const +{ + return m_pathRootNode == nullptr || m_pathRootNode->isEmpty(flagsToCheck); +} + +bool JsonPath::isAggregateMethod(const PathMethod method) +{ + switch (method) + { + case PathMethod::SIZE: + case PathMethod::TYPE: + return true; + default: + return false; + } +} + +bool JsonPath::isUnwrapMethod(const PathMethod method) +{ + switch (method) + { + case PathMethod::NONE: + case PathMethod::SIZE: + case PathMethod::TYPE: + return false; + default: + return true; + } +} + + +JsonPathExpr::JsonPathExpr(MemoryPool& pool) : + Firebird::PermanentStorage(pool), + m_jpath(FB_NEW_POOL(pool) JsonPath(pool)) +{ } + +JsonPathExpr::~JsonPathExpr() +{ } + +void JsonPathExpr::resetExpr(JsonExprNode* tail, JsonExprNode* arithmetic) +{ + // Omit empty arithmetic path (it contains only a arithmetic variable) + // tail is null when the input is a passing variable. Example: json_value('"test"', '$passed_timestamp') + if (tail != nullptr && ((tail->isVariable() && !tail->hasTail()) || tail->isRootEmpty())) + { + delete tail; + tail = nullptr; + } + m_tail = tail; + + fb_assert(arithmetic); + if ((arithmetic->isVariable() && arithmetic->getVariable()->type == PathVariable::Type::ROOT) || arithmetic->isRootEmpty()) + { + delete arithmetic; + arithmetic = nullptr; + } + m_math = arithmetic; +} + +const JsonExprNode* JsonPathExpr::getMath() const +{ + return m_math; +} + +const JsonExprNode* JsonPathExpr::getTail() const +{ + return m_tail; +} + +// PathNode + +PathNode::PathNode(MemoryPool& pool, PathNode* prev) + : Firebird::PermanentStorage(pool), + field(pool), ranges(pool), prev(prev) +{ + if (prev) + { + fb_assert(prev->next == nullptr); + + prev->next = this; + depth = prev->depth + 1; + } +} + +PathNode::~PathNode() +{ + PathNode* cur = next; + PathNode* nnext; + while (cur) + { + nnext = cur->next; + cur->next = nullptr; + delete cur; + cur = nnext; + } +} + +PathNode* PathNode::update() +{ + if (isTemporary()) + { + PathNode* newCurrent = next ? next : prev; + + fb_assert(prev); + prev->filterNode.reset(filterNode.release()); + removeThis(); + return newCurrent; + } + else + enable(); + + return this; +} + +PathNode* PathNode::insertUnwrapNode() +{ + insert(); + next->state = PathNodeState::TEMPORARY; + next->matched = false; + next->filterNode = std::move(filterNode); + next->selectAll(); + + return next; +} + +void PathNode::removeThis() noexcept +{ + if (next) + { + next->prev = prev; + PathNode* temp = next; + while (temp) + { + --temp->depth; + temp = temp->next; + } + } + + if (prev) + { + if (isTemporary()) + prev->filterNode = std::move(filterNode); + + prev->next = next; + } + + next = nullptr; + prev = nullptr; + delete this; +} + +bool PathNode::equalsNoComplex(const JsonLevelNode* const jsonLevel) +{ + // Previous level should be matched + PathNode* enabledPrev = getEnabledPrev(); + if (enabledPrev == nullptr || !enabledPrev->matched || jsonLevel->depth > depth) + return false; + + matched = false; + switch (getSuperType(type, jsonLevel->itemType)) + { + case getSuperType(ItemType::FIELD, ItemType::FIELD): + if (!matchFieldName(jsonLevel->field)) + return false; + + // Check the previous state because the current one can be skipped (when an unwrapping node has been inserted) + fb_assert(enabledPrev->filterNode == nullptr); + matched = true; + break; + case getSuperType(ItemType::FIELD, ItemType::ARRAY_ELEMENT): + if (enabledPrev->matchUnwrapPattern()) + { + // $.field + // [{"filed":42}] + // Expected object but encoutred an array + + // Modify path + // $.field => $[*].field + enabledPrev->insertUnwrapNode(); + enabledPrev->next->matched = true; + } + return false; + case getSuperType(ItemType::ARRAY_ELEMENT, ItemType::ARRAY_ELEMENT): + if (!isInSimpleRange(jsonLevel->indexInArray)) + return false; + + fb_assert(enabledPrev->filterNode == nullptr); + matched = true; + break; + case getSuperType(ItemType::ARRAY_ELEMENT, ItemType::FIELD): + if (next && matchWrapPattern()) + { + fb_assert(!next->isDisabled()); + + // $[0].a + // {"a":42} + matched = true; + return next->equalsNoComplex(jsonLevel); + } + return false; + + default: + fb_assert(false); + } + + return isNextEmpty() && jsonLevel->depth == depth; // reached end +} + + +bool PathVariable::hasPath(const UCHAR flagsMask) const +{ + return path.hasData() && !path->isEmpty(flagsMask); +} diff --git a/src/jrd/json/path/JsonPath.h b/src/jrd/json/path/JsonPath.h new file mode 100644 index 00000000000..3c1da52303f --- /dev/null +++ b/src/jrd/json/path/JsonPath.h @@ -0,0 +1,699 @@ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonPath.h + * DESCRIPTION: JSON Path parser. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Denis Logashov + * for Red Soft Corporation. + * + * Copyright (c) 2017 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#ifndef JSON_PATH_H +#define JSON_PATH_H + +#include "../classes/JsonTypes.h" + +#include "../common/classes/init.h" + +namespace FBJSON { + +inline constexpr PathArrayIndex JPATH_ARRAY_LAST_INDEX = -1; + + +struct PathNode; +class JsonExprNode; +class JsonPath; +class PathParser; +class PassingList; +struct PathVariable; +struct PathInjection; + + +class JsonPath : Firebird::PermanentStorage +{ + enum Flags : UCHAR + { + FLAG_STRICT = 1, // lax by default + FLAG_HAS_INDEX = 2, + FLAG_HAS_FIELD = 4, + FLAG_COMPLEX_RANGE = 8, + }; + +public: + struct Range + { + public: + PathArrayIndex down = 0; + PathArrayIndex up = 0; + + constexpr void init(const PathArrayIndex singleIndex = 0) noexcept + { + down = singleIndex; + up = singleIndex; + } + + constexpr void init(const PathArrayIndex down, const PathArrayIndex up) noexcept + { + this->down = down; + this->up = up; + } + + // Support negative indexes + // Return signed because the index can be negative even after array size adding + constexpr JsonArrayIndex getPreparedUp(const ArraySize size) const noexcept + { + return up >= 0 ? up : PathArrayIndex(size) + up; + } + + // Return signed because the index can be negative even after array size adding + constexpr JsonArrayIndex getPreparedDown(const ArraySize size) const noexcept + { + return down >= 0 ? down : down + static_cast(size); + } + + // Simple range + constexpr bool enters(const PathArrayIndex index) const + { + fb_assert(!isComplex()); + fb_assert(down >= 0); + fb_assert(up >= JPATH_ARRAY_LAST_INDEX); + return index >= down && (up == JPATH_ARRAY_LAST_INDEX || index <= up); + } + + // When a range can get first element, it is possible a wrapper + // Example: the JSON '{"data":42}' with 'lex $[0].a' is equals to [{"data":42}] + constexpr bool canWrap() const noexcept + { + // Only -1 is a valid negative value so array size does not matter here + return down == 0 || up == JPATH_ARRAY_LAST_INDEX || up == 0; + } + + inline bool canGetFirst(const ArraySize arraySize) const noexcept + { + return down == 0 || getPreparedUp(arraySize) == 0; + } + + constexpr bool isComplex() const noexcept + { + // Allow wildcard (*), simple ranges (1 to 2, 0 to last) and non negative indexes (2, 39, 0) + // `last` token and all the negative values are consider as complex + if (isWildcard()) + return false; + + return down < 0 || up < 0; + } + + constexpr bool isWildcard() const noexcept + { + return down == 0 && up == JPATH_ARRAY_LAST_INDEX; + } + }; + + JsonPath(MemoryPool& pool, const bool createPath = true); + + JsonPath(JsonPath&&) noexcept = delete; + JsonPath(const JsonPath&) = delete; + + JsonPath& operator=(JsonPath&&) noexcept = delete; + JsonPath& operator=(const JsonPath&) = delete; + + ~JsonPath(); + + void unwrap(); + + PathNode* resetRootNode(); + PathInjection& addInjection(); + + const PathNode* getRootNode() const noexcept + { + return m_pathRootNode.get(); + } + + PathNode* getRootNode() noexcept + { + return m_pathRootNode.get(); + } + +public: + inline bool hasFields() const noexcept + { + return flags & FLAG_HAS_FIELD; + } + + inline bool hasIndexes() const noexcept + { + return flags & FLAG_HAS_INDEX; + } + + inline bool hasComplexRange() const noexcept + { + return flags & FLAG_COMPLEX_RANGE; + } + + inline bool isLax() const noexcept + { + return (flags & FLAG_STRICT) == 0; + } + + inline bool isStrict() const noexcept + { + return flags & FLAG_STRICT; + } + + bool isZeroPath() const; + bool isWrappingZeroPath() const; + bool isEmpty(const UCHAR flagsToCheck) const; + + inline void setIndexesFlag() + { + flags |= FLAG_HAS_INDEX; + } + + inline void setComplexRangeFlag() + { + flags |= FLAG_COMPLEX_RANGE; + } + + inline void setFieldsFlag() + { + flags |= FLAG_HAS_FIELD; + } + + inline void setStrictFlag() + { + flags |= FLAG_STRICT; + } + +public: + static bool isAggregateMethod(const PathMethod method); + static bool isUnwrapMethod(const PathMethod method); + +protected: + UCHAR flags = 0; + +private: + Firebird::AutoPtr m_pathRootNode; + + // Instead of a field or an index, an expression can be used. + // But in 99% of cases, this is not the case. So use simple scalars in the PathNode's and + // inject calculated value if needed before the processing + Firebird::ObjectsArray m_injections; +}; + + +// Store parsed JSON Path in a 3 components +// +// is the main root path. +// are the nodes used to modify output value of its path +// are the nodes used to apply arithmetical expressions to the node +// $.value.keyvalue().value + $.mod + 4 +// <-----> <--------------> <---------> +// primary tail arithmetic + +// In case of sequence, arithmetic applies to each sequence element + +class JsonPathExpr : public Firebird::PermanentStorage +{ +public: + JsonPathExpr(MemoryPool& pool); + + JsonPathExpr(JsonPathExpr&&) noexcept = delete; + JsonPathExpr(const JsonPathExpr&) = delete; + + JsonPathExpr& operator=(JsonPathExpr&&) noexcept = delete; + JsonPathExpr& operator=(const JsonPathExpr&) = delete; + + ~JsonPathExpr(); + + const JsonExprNode* getMath() const; + const JsonExprNode* getTail() const; + inline const JsonPath* getJsonPath() const noexcept + { + return m_jpath.get(); + } + + inline JsonPath* getJsonPath() noexcept + { + return m_jpath.get(); + } + + // arithmetic(.) + void resetExpr(JsonExprNode* tail, JsonExprNode* arithmetic); + inline bool hasMath() const + { + return getMath() != nullptr; + } + +private: + Firebird::AutoPtr m_jpath; + Firebird::AutoPtr m_tail; // Accepts to each element + Firebird::AutoPtr m_math; // Accepts to output expression +}; + +using RangesArray = Firebird::HalfStaticArray; + + +enum class PathNodeState : UCHAR +{ + NORMAL = 0, + TEMPORARY, + DISABLED +}; + +// Each part of a json path will be converted into a PathNode. +// We could use just an array but for lax mode it may be necessary to insert a node +// For example: +// ($.price.value) => node -> node('price') -> node('value'); +// ($[0].value) => node(ranges:[0]) -> node('value'); +// When price is an object the path wont change but for an array we will insert an extra node: +// node('price')->node('value') ==> node('price') -> node([*]) -> node('value') +struct PathNode : Firebird::PermanentStorage +{ +public: + PathNode(MemoryPool& pool, PathNode* prev = nullptr); + + PathNode(PathNode&&) noexcept = delete; + PathNode(const PathNode&) = delete; + + PathNode& operator=(PathNode&&) = delete; + PathNode& operator=(const PathNode&) = delete; + ~PathNode(); + +public: // Node modification methods + inline void enable() noexcept + { + if (state != PathNodeState::DISABLED) + return; + + state = PathNodeState::NORMAL; + if (next == nullptr) + return; + + PathNode* it = next; + it->prev = this; + while (it) + { + ++it->depth; + it = it->next; + } + } + + inline void disable() noexcept + { + if (isDisabled()) + return; + + state = PathNodeState::DISABLED; + + if (next == nullptr) + return; + + PathNode* it = next; + next->prev = prev; + + while (it) + { + --it->depth; + it = it->next; + } + } + + inline void selectAll() + { + type = ItemType::ARRAY_ELEMENT; + ranges.add().init(0, JPATH_ARRAY_LAST_INDEX); + } + + PathNode* update(); + + inline void updateLaxPatterns() + { + for (FB_SIZE_T i = 0; i < ranges.getCount(); i++) + { + if (ranges[i].canWrap()) + { + flags |= PathNode::FLAG_WRAP; + break; + } + } + } + + inline PathNode* add() + { + fb_assert(next == nullptr); + return next = FB_NEW_POOL(getPool()) PathNode(getPool(), this); + } + + PathNode* insertUnwrapNode(); + + inline PathNode* insert() + { + if (next == nullptr) + return add(); + else + { + PathNode* oldNext = next; + // this<-->oldNext + next = nullptr; + PathNode* newNext = FB_NEW_POOL(getPool()) PathNode(getPool(), this); + newNext->next = oldNext; + oldNext->prev = newNext; + // this<-->newNext<-->oldNext + + while (oldNext) + { + ++oldNext->depth; + oldNext = oldNext->next; + } + + next = newNext; + return newNext; + } + } + + void removeThis() noexcept; + + inline PathNode* moveTo(const SSHORT newDepth) noexcept + { + if (depth == newDepth) + { + if (isDisabled()) + { + return prev; + } + else + return this; + } + else if (depth > newDepth) + { + // Move Down + + PathNode* temp = this; + while (temp && temp->depth > newDepth) + { + temp->matched = false; + temp = temp->prev; + } + + if (temp && temp->state != PathNodeState::TEMPORARY) + temp->matched = false; + + return temp; + } + else + { + // Move Up + + // Don't check for update because the state adds in a previous level to the next one + PathNode* temp = this; + while (temp && temp->depth < newDepth) + { + temp = temp->next; + } + + while (temp && temp->isDisabled()) + { + temp = temp->next; + } + + return temp; + } + } + +public: // Checkers + // Check the range to match the array size + // Throw an error on fail + void validateRange(const ArraySize arraySize) const + { + for (USHORT i = 0; i < ranges.getCount(); ++i) + { + // Current jsonLevel is an element level. Get size from parent (array) level + const PathArrayIndex rangeA = ranges[i].getPreparedDown(arraySize); + const PathArrayIndex rangeB = ranges[i].getPreparedUp(arraySize); + + // [4,2] for example + if (rangeA > rangeB) + { + json_strict_exception::raise(JsonStatusMsg(isc_jstrict_common) << + JsonStatusMsg(isc_jstrict_invalid_range) << SINT64(rangeA) << SINT64(rangeB)); + } + + // [-20,-10] for example + if (rangeA < 0 || rangeA >= PathArrayIndex(arraySize) || rangeB >= PathArrayIndex(arraySize)) + { + const SINT64 printSize = arraySize > 0 ? (arraySize - 1) : 0; + + json_strict_exception::raise(JsonStatusMsg(isc_jstrict_common) << + JsonStatusMsg(isc_jstrict_out_of_range) << + SINT64(rangeA) << SINT64(rangeB) <field == Tokens::ANY_FIELD) + return true; + + if (this->field == name) + return true; + + return false; + } + + inline bool isInSimpleRange(const JsonArrayIndex index) const + { + if (ranges.isEmpty()) + return false; + + for (ULONG i = 0; i < ranges.getCount(); i++) + { + if (ranges[i].enters(index)) + { + return true; + } + } + + return false; + } + + inline bool isNextEmpty() const noexcept + { + // Two diabled in a row is impossible but check it anyway + return next == nullptr || (next->isDisabled() && next->isNextEmpty()); + } + + // No complex ranges should be in the node + // Complex ranges should be checked via JSON BINARY to avoid extra parsing to calculate array size + bool equalsNoComplex(const JsonLevelNode* const jsonLevel); + + bool equalsZeroDepth(const JsonLevelNode* jsonLevel) const noexcept + { + return depth == 0 && jsonLevel->depth == 0; + } + + bool canWrap(const JsonLevelNode* jsonLevel) const noexcept + { + // We do not add (wrap) json level, instead modify the path. + // Example: for '{"data":{"a":1}}' we can convert '$.data[0].a to $.data.a + if (isNextEmpty()) + return false; + + return matched && next->matchWrapPattern() && jsonLevel->depth == depth; + } + + inline bool canWrapEnd(const JsonLevelNode* jsonLevel) const + { + // We do not add (wrap) json level, instead modify the path. + // Example: for '{"data":1}' we can convert '$.data[0].a to $.data + return canWrap(jsonLevel) && next->isNextEmpty(); + } + + inline bool canUnwrapEnd() noexcept + { + return matched && next == nullptr && matchUnwrapPattern(); + } + + inline bool canUnwrapMiddle(const JsonLevelNode* jsonLevel) const noexcept + { + // Can insert [*] to path + return matched && next && jsonLevel->isArray() && matchUnwrapPattern(); + } + + inline bool isTemporary() const noexcept + { + return state == PathNodeState::TEMPORARY; + } + + inline bool isDisabled() const noexcept + { + return state == PathNodeState::DISABLED; + } + + inline bool isEmpty(const USHORT flagsToCheck) const + { + return next == nullptr && ((flags & flagsToCheck) == 0) && !filterNode.hasData(); + } + + inline bool matchWrapPattern() const noexcept + { + return flags & FLAG_WRAP; + } + + inline bool matchUnwrapPattern() const noexcept + { + return flags & FLAG_UNWRAP; + } + +public: // Getters + inline PathNode* getEnabledPrev() const + { + return (prev && prev->isDisabled() ? prev->getEnabledPrev() : prev); + } + +private: + friend class JsonPath; + +public: + enum LexPattern + { + FLAG_NONE = 0, + FLAG_WRAP = 1, + FLAG_UNWRAP = 2, + FLAG_PROCESSED = 4, + FLAG_ALL_LEX = FLAG_WRAP | FLAG_UNWRAP + }; + + // Common data + ItemType type = ItemType::FIELD; + SSHORT depth = 0; + UCHAR flags = 0; + Firebird::AutoPtr filterNode; + + // Filed + SmallString field; + + // Range + RangesArray ranges; + + // Common + PathNode* next = nullptr; + PathNode* prev = nullptr; + + // Mutable fields + PathNodeState state = PathNodeState::NORMAL; + bool matched = false; +}; + + +struct PathVariable : private Firebird::PermanentStorage +{ + enum class Type + { + JSON = 0, // The source json + ITEM, // Root in a filter + PASSING, + ROOT, // Calculated primary root variable to pass to the path tail expr + HEAD + }; + + SmallString name; + Firebird::AutoPtr path; + Type type = Type::ITEM; + + PathVariable(MemoryPool& pool, const Type type = Type::ITEM) + : Firebird::PermanentStorage(pool), + name(pool), + type(type) + { } + + PathVariable(PathVariable&&) noexcept = delete; + PathVariable(const PathVariable&) = delete; + + PathVariable& operator=(PathVariable&&) noexcept = delete; + PathVariable& operator=(const PathVariable&) = delete; + + ~PathVariable() = default; + + inline bool isItem() const noexcept + { + return type == Type::ITEM; + } + + inline bool isPassing() const noexcept + { + return type == Type::PASSING; + } + + bool hasPath(const UCHAR flagsMask) const; +}; + + +// Use it to calculate and inject result into JSON Path +struct PathInjection : private Firebird::PermanentStorage +{ + enum class Position + { + FIELD = 0, + RANGE_UP, + RANGE_DOWN, + RANGE_INDEX + }; + + // Where to inject + Position position = Position::FIELD; + SSHORT rangeIndex = -1; + SSHORT depth = -1; + + // What to inject + Firebird::AutoPtr expr; + + PathInjection(MemoryPool& pool) + : Firebird::PermanentStorage(pool) + { } + + PathInjection(PathInjection&&) noexcept = delete; + PathInjection(const PathInjection&) = delete; + + PathInjection& operator=(PathInjection&&) noexcept = delete; + PathInjection& operator=(const PathInjection&) = delete; + + ~PathInjection() = default; +}; + +} + +#endif // JSON_PATH_H diff --git a/src/jrd/json/path/JsonPathParser.y b/src/jrd/json/path/JsonPathParser.y new file mode 100644 index 00000000000..b23b4b4fc19 --- /dev/null +++ b/src/jrd/json/path/JsonPathParser.y @@ -0,0 +1,800 @@ +%{ +/* + * PROGRAM: Firebird JSON logic. + * MODULE: JsonPathParser.y + * DESCRIPTION: JSON Path parser. + * + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.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.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl. + * + * Software distributed under the License is distributed AS IS, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. + * See the License for the specific language governing rights + * and limitations under the License. + * + * The Original Code was created by Artyom Abakumov + * for Red Soft Corporation. + * + * Copyright (c) 2023 Red Soft Corporation + * and all contributors signed below. + * + * All Rights Reserved. + * Contributor(s): ______________________________________. + */ + +#include "firebird.h" +#include "../jrd/json/JsonRuntime.h" +#include "../jrd/json/path/JsonPath.h" +#include "../jrd/json/JsonConsts.h" + +#define YYREDUCEPOSNFUNC yyReducePosn +#define YYREDUCEPOSNFUNCARG NULL + + +// ASF: Inherited attributes (aka rule parameters) are executed even when in trial mode, but action +// rules ({}) are executed only when in full parse mode. NOTRIAL should be used to avoid segfaults +// due to accessing invalid pointers in parameters (not yet returned from action rules). +#define NOTRIAL(x) (yytrial ? NULL : (x)) + + +#include "../dsql/chars.h" + +using namespace Firebird; +using namespace FBJSON; + +%} + + +// token declarations + +%token INTEGER16 + +%token INTEGER32 +%token INTEGER64 +%token DOUBLE + +%token JSON_NULL +%token TRUE +%token FALSE + +%token STRING +%token QUOTED_STRING +%token PASSING_NAME +%token LAX +%token STRICT +%token LAST +%token TO +%token KEYVALUE +%token DATETIME +%token DECIMAL +%token TIME +%token TIME_TZ +%token TIMESTAMP +%token TIMESTAMP_TZ + +%token METHOD_DOT // Special dot that checks bracket: '.' method '(' + +%token METHOD + + +%token STARTS +%token WITH +%token LIKE_REGEX +%token FLAGS +%token IS +%token UNKNOWN +%token EXISTS + +%token AND +%token OR +%token EQ +%token NEQ +%token LEQ +%token GEQ + + +// precedence declarations for expression evaluation + +%left OR +%left AND +%left '!' +%left EQ NEQ '<' '>' LIKE_REGEX STARTS NEQ GEQ LEQ +%left IS // IS UNKNOWN +%left '+' '-' +%left '*' '/' '%' +%left UMINUS UPLUS + +%union YYSTYPE +{ + YYSTYPE() + {} + + bool boolVal; + SLONG int32Val; + SINT64 int64Val; + double doubleVal; + Firebird::string* stringPtr; + struct DecimalPair + { + FBJSON::JsonExprNode* scale; + FBJSON::JsonExprNode* precision; + } pairVal; + + SSHORT int16Val; + Firebird::PodOptional int16ValOpt; + FBJSON::PathMethod methodType; + FBJSON::JsonExprNode* exprNode; + FBJSON::PathVariable* variable; + FBJSON::JsonExprOperation exprOperation; + FBJSON::PathInjection::Position passingPos; + FBJSON::JsonPathContext* jpathContext; +} + +%include jtypes.y + + +%% + + +// One possible statement +top + : json_path + ; + +json_path + : path_mode json_path_arithmetic_finished + { setPrimary($2); } + ; + +path_mode + : /* nothing */ { m_isLaxMode = true; } + | LAX { m_isLaxMode = true; } + | STRICT + { + m_isLaxMode = false; + m_output->getJsonPath()->setStrictFlag(); + } + ; + +// ********************** +// Primary Path rules +// ********************** + +// ($ | @ | $passing) [. ] +%type variable_accessor +variable_accessor + : '$' + { $$ = createMainContext(); } + | '@' + { $$ = createVariableContext(PathVariable::Type::ITEM); } + | PASSING_NAME + { $$ = createPassingContext(*$1); } + ; + +// @ | $passing +%type json_variable +json_variable + : variable_accessor variable_body_opt($1) + { $$ = $2; } + ; + + +// The primary path side, before the filter +// <$ with path> | $passing | | +%type path_primary +path_primary + : number_scalar // It is easier to place it here and check instead of adding separate rule + { $$ = $1; } + | QUOTED_STRING + { $$ = mkNode()->set(*$1); } + | json_variable + { $$ = mkVariableNode($1); } + ; + +%type path_accessor +path_accessor + : path_primary %prec UMINUS + { + $$ = $1; + } + | path_accessor path_method // reduce/reduce conflict due to '.' + { + $$ = addOptTailNode($1, $2); + } + | path_accessor filter_expr // reduce/reduce conflict due to '?' + { + if (m_isLaxMode) + unwrapPrimary($1); + + JsonExprNode* exprNode = mkNode(); + exprNode->makeHeadNode(false); + exprNode->addChild($2); + + $$ = addOptTailNode($1, exprNode); + } + | '(' json_path_arithmetic ')' + { + $$ = $2; + } + ; + + +%type path_accessor_compiled +path_accessor_compiled + : path_accessor + { + $$ = completePath($1); + } + ; + + +// ['-'] +%type json_path_expr +json_path_expr + : path_accessor_compiled + { + auto* node = mkNode(); + node->addChild($1); + $$ = node; + } + | arithmetic_unary path_accessor_compiled + { + if ($2->applyUnaryOp($1)) + { + $$ = $2; // Just a scalar + } + else + { + if (m_isLaxMode) + unwrapPrimary($2); + + JsonExprNode* node = mkNode(); + node->addOperation($1); + node->addChild($2); + $$ = node; + } + } + ; + +%type arithmetic_unary +arithmetic_unary + : '+' %prec UPLUS { $$ = JsonExprOperation::UNARY_PLUS; } + | '-' %prec UMINUS { $$ = JsonExprOperation::UNARY_MINUS; } + ; + +// ( | ) [ ( | )] +%type json_path_arithmetic +json_path_arithmetic + : json_path_expr + { + $$ = $1; + } + | json_path_arithmetic arithmetic_operator json_path_expr + { + if (m_isLaxMode) + { + $1->unwrap(); + $3->unwrap(); + } + + $1->addOperation($2); + $1->addChild($3); + $$ = $1; + } + ; + +%type json_path_arithmetic_finished +json_path_arithmetic_finished + : json_path_arithmetic + { + $$ = $1->finish(); + } + +%type arithmetic_operator +arithmetic_operator + : '+' { $$ = JsonExprOperation::ADDITION; } + | '-' { $$ = JsonExprOperation::SUBTRACTION; } + | '*' { $$ = JsonExprOperation::MULTIPLICATION; } + | '/' { $$ = JsonExprOperation::DIVISION; } + | '%' { $$ = JsonExprOperation::MODULO; } + ; + +// ********************** +// Any path body +// ********************** + +%type keyvalue_path_opt() +keyvalue_path_opt($context) + : path_body_opt($context) + ; + +%type variable_body_opt() +variable_body_opt($context) + : path_body_opt($context) + { $$ = $context->variable; } + ; + +%type path_body_opt() +path_body_opt($context) + : /*nothing*/ + | path_filer($context) + | path_filer($context) path_body_accessors($context) + | path_body_accessors($context) + ; + +%type path_body_accessors() +path_body_accessors($context) + : accessor_with_opt_filter($context) %prec UMINUS // prec to solve reduce/reduce conflicts + | path_body_accessors accessor_with_opt_filter($context) + ; + +%type accessor_with_opt_filter() +accessor_with_opt_filter($context) + : accessor($context) path_filter_opt($context) + ; + +%type path_filter_opt() +path_filter_opt($context) + : /* Nothing */ + | path_filer($context) + ; + +%type path_filer() +path_filer($context) + : filter_expr + { + if (m_isLaxMode) + $context->current->flags |= PathNode::FLAG_UNWRAP; + + $context->main->setComplexRangeFlag(); + $context->current->filterNode = $1; + } + ; + + +// Range or object +%type accessor() +accessor($context) + : '.' object($context) + { + // Mark prev level + if (m_isLaxMode && $context->current->type == ItemType::FIELD) + $context->current->flags |= PathNode::FLAG_UNWRAP; + + // Add after in case of PathMethod + $context->current = $context->current->add(); + $context->current->type = ItemType::FIELD; + + $context->current->field = *$2; + + $context->main->setFieldsFlag(); + } + | '[' ranges($context) ']' + { + $context->current = $context->current->add(); + $context->current->type = ItemType::ARRAY_ELEMENT; + + const USHORT size = $context->current->ranges.getCount(); + + bool isComplex = false; + for (USHORT i = 0; i < size; i++) + { + const auto& range = $context->current->ranges[i]; + if (m_isLaxMode && range.canWrap()) + $context->current->flags |= PathNode::FLAG_WRAP; + + if (range.isComplex()) + isComplex = true; + } + + if (size > 0) + { + $context->main->setIndexesFlag(); + } + + if (isComplex || size > 1) + $context->main->setComplexRangeFlag(); + } + ; + + +// Object types +%type object() +object($context) + : identifier { $$ = $1; } + | '*' { $$ = mkTempString("*"); } + | PASSING_NAME + { + auto& injection = addPathInjection($context, *$1); + injection.position = PathInjection::Position::FIELD; + + $$ = mkTempString(""); + } + | '(' json_path_arithmetic_finished ')' + { + auto& injection = addPathInjection($context, $2); + injection.position = PathInjection::Position::FIELD; + + $$ = mkTempString(""); + } + ; + +%type identifier +identifier + : STRING { $$ = $1;} + | QUOTED_STRING { $$ = $1;} + ; + + +// Range types +%type ranges() +ranges($context) + : {$context->clearRange(); } indexes($context) + ; + +%type indexes() +indexes($context) + : index($context) + | indexes ',' index($context) + ; + +%type index() +index($context) + : single_index($context, PathInjection::Position::RANGE_INDEX) range_opt($context) + { + const auto endRange = $2.toOptional(); + if (endRange == std::nullopt) + $context->addRange($1); + else + $context->addRange($1, endRange.value()); + } + | '*' + { $context->addRange(0, -1); } + ; + +// Use TO instead of TO to resolve conflicts +%type range_opt() +range_opt($context) + : /* nothing */ + { $$ = std::nullopt; } + | TO single_index($context, PathInjection::Position::RANGE_UP) + { $$ = $2; } + ; + +%type single_index(, ) +single_index($context, $passType) + : LAST + { $$ = -1; } + | LAST '-' INTEGER16 + { $$ = -1 - $3; } + | LAST '+' INTEGER16 // Allow this obvious out of range value in case of modification (appending element) + { $$ = -1 + $3; } + | json_path_arithmetic_finished // Get number or an expression + { + auto rangeNumber = $1->getRangeNumber(); + if (rangeNumber != std::nullopt) + { + $$ = rangeNumber.value(); + } + else + { + auto& injection = addPathInjection($context, $1); + injection.position = $passType; + injection.rangeIndex = $context->current->ranges.getCount(); + + $$ = 1; // Return non zero to do not match unwrap pattern + } + } + ; + + +// ********************** +// Methods types +// ********************** + +// Any scalar type (number, string...) +%type scalar_method +scalar_method + : no_arg_method { $$ = $1; } + | datetime_methods { $$ = $1; } + /* | decimal_method { $$ = $1; } */ + ; + +// All the standard types are digital-oriented +%type no_arg_method +no_arg_method + : METHOD_DOT METHOD '(' ')' + { + // The METHOD token includes the first '(' + $$ = mkMethodNode($2); + } + ; + +// Include JSON-oriented types like Array and Object +%type path_method +path_method + : scalar_method + { $$ = $1; } + | METHOD_DOT KEYVALUE '(' ')' { $$ = createContext(); } /* 1-5 */ keyvalue_path_opt($5) //6 + { + JsonExprNode* tail = nullptr; + + if (!$5->isEmpty()) + { + PathVariable* var = FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::HEAD); + var->path.reset($5->extract()); + + tail = mkVariableNode(var); + } + + $$ = mkMethodNode(PathMethod::KEYVALUE, tail); + } + ; + + +%type time_json_method +time_json_method + : TIME {$$ = PathMethod::TIME;} + | TIME_TZ {$$ = PathMethod::TIME_TZ;} + | TIMESTAMP {$$ = PathMethod::TIMESTAMP;} + | TIMESTAMP_TZ {$$ = PathMethod::TIMESTAMP_TZ;} + ; + +%type datetime_methods +datetime_methods +// : METHOD_DOT time_json_method '(' time_precision_opt ')' // Currently, time precision is not supported in SQL +// { $$ = mkMethodNode($2, $4); } + : METHOD_DOT time_json_method '(' ')' + { $$ = mkMethodNode($2, nullptr); } + | METHOD_DOT DATETIME '(' datetime_template_opt ')' + { $$ = mkMethodNode(PathMethod::DATETIME, $4); } + ; + +%type decimal_method +decimal_method + : METHOD_DOT DECIMAL '(' decimal_arguments ')' + { + auto* methodNode = mkMethodNode(PathMethod::DECIMAL); + if ($4.precision) + { + $4.precision->addFlag(JsonExprNode::FLAG_METHOD_ARGUMENT); + methodNode->addChild($4.precision); + + if ($4.scale) + { + $4.scale->addFlag(JsonExprNode::FLAG_METHOD_ARGUMENT); + methodNode->addChild($4.scale); + } + } + + $$ = methodNode; + } + ; + +%type decimal_arguments +decimal_arguments + : //nothing + { $$ = {nullptr, nullptr}; } + | json_decimal_arg // precision + { $$ = {$1, nullptr}; } + | json_decimal_arg ',' json_decimal_arg // precision, scale + { $$ = {$1, $3}; } + ; + + +%type json_decimal_arg +json_decimal_arg + : INTEGER32 + { + $$ = mkNode()->set((int)$1); + $$->addFlag(JsonExprNode::FLAG_METHOD_ARGUMENT); + } + ; + +// ********************** +// Filter rules +// ********************** + +%type filter_expr +filter_expr + : '?' { m_parseInFilter = true; } '(' boolean_expression ')' { m_parseInFilter = false; $$ = $4; } + ; + +// Boolean exprs + +%type value_to_compare +value_to_compare + : json_path_arithmetic_finished + { $$ = $1; } + | boolean + { $$ = mkNode()->set($1); } + | JSON_NULL + { $$ = mkNode()->setNull(); } + ; + + +%type boolean_expression +boolean_expression + : unary_predicate + { $$ = $1; } + | boolean_expression bvb_comparison unary_predicate // shift Conflict + { + auto* root = mkNode(); + root->addChild($1); + root->addOperation($2); + root->addChild($3); + $$ = root->finish(); + } + ; + + +// bool vs bool +%type bvb_comparison +bvb_comparison + : OR { $$ = JsonExprOperation::OR; } + | AND { $$ = JsonExprOperation::AND; } + ; + +%type unary_predicate +unary_predicate + : predicate { $$ = $1; } + | not_arg predicate + { + auto* root = mkNode(); + root->addOperation($1); + root->addChild($2); + $$ = root->finish(); + } + ; + +%type predicate +predicate + : comparison_predicate { $$ = $1; } + | filter_function { $$ = $1; } + | '(' boolean_expression ')' { $$ = $2; } + ; + +%type comparison_predicate +comparison_predicate + : value_to_compare comparison_operator value_to_compare + { + if (m_isLaxMode) + { + $1->unwrap(); + $3->unwrap(); + } + + JsonExprNode* root = mkNode(); + root->addChild($1); + root->addOperation($2); + root->addChild($3); + $$ = root->finish(); + } + ; + +%type filter_function +filter_function + : function_str_arg STARTS WITH function_str_arg + { + if (m_isLaxMode) + $1->unwrap(); + + auto* root = mkNode(); + root->addChild($1); + root->addOperation(JsonExprOperation::STARTS_WITH); + root->addChild($4); + $$ = root->finish(); + } + | function_str_arg LIKE_REGEX function_str_arg regex_flags_opt + { + if (m_isLaxMode) + $1->unwrap(); + + auto* root = mkNode(); + root->addChild($1); + root->addOperation(JsonExprOperation::LIKE_REGEX); + root->addChild($3); + if ($4) + { + root->addOperation(JsonExprOperation::REGEX_FLAGS); + root->addChild($4); + } + + $$ = root->finish(); + } + | EXISTS '(' json_path_arithmetic_finished ')' + { + auto* root = mkNode(); + root->addOperation(JsonExprOperation::EXISTS); + root->addChild($3); + $$ = root->finish(); + } + | '(' boolean_expression ')' IS UNKNOWN + { + auto* root = mkNode(); + root->addChild($2); + root->addOperation(JsonExprOperation::IS_UNKNOWN); + $$ = root->finish(); + } + ; + +// Allow invalid types in case of some kind auto generated path +%type function_str_arg +function_str_arg + : path_accessor + { $$ = $1; } + ; + +%type datetime_template_opt +datetime_template_opt + : /* nothing */ { $$ = nullptr; } + | function_str_arg + { + $$ = $1; + $$->addFlag(JsonExprNode::FLAG_METHOD_ARGUMENT); + } + ; + +%type regex_flags_opt +regex_flags_opt + : // nothing + { $$ = nullptr; } + | FLAGS QUOTED_STRING + { $$ = mkNode()->set(*$2); } + ; + +%type comparison_operator +comparison_operator + : EQ { $$ = JsonExprOperation::EQUALS; } + | NEQ { $$ = JsonExprOperation::NOT_EQUALS; } + | '>' { $$ = JsonExprOperation::MORE; } + | '<' { $$ = JsonExprOperation::LESS; } + | GEQ { $$ = JsonExprOperation::MORE_OE; } + | LEQ { $$ = JsonExprOperation::LESS_OE; } + ; + +%type not_arg +not_arg + : '!' { $$ = JsonExprOperation::NOT; } + ; + + +%type number_scalar_pos +number_scalar_pos + : INTEGER16 { $$ = mkNode()->set((int)$1); } + | INTEGER32 { $$ = mkNode()->set((int)$1); } + | INTEGER64 { $$ = mkNode()->set($1); } + | DOUBLE { $$ = mkNode()->set($1); } + ; + +%type number_scalar +number_scalar + : number_scalar_pos { $$ = $1; } + | '-' number_scalar_pos { $$ = $2; $$->applyUnaryOp(JsonExprOperation::UNARY_MINUS); } + ; + +%type time_precision_opt +time_precision_opt + : { $$ = nullptr; } + | time_precision { $$ = $1; } + ; + +%type time_precision +time_precision + : INTEGER16 + { + $$ = mkNode()->set((int)$1); + $$->addFlag(JsonExprNode::FLAG_METHOD_ARGUMENT); + } + ; + +%type boolean +boolean + : TRUE { $$ = true; } + | FALSE { $$ = false; } + ; + +%% diff --git a/src/jrd/json/path/btyacc_json.ske b/src/jrd/json/path/btyacc_json.ske new file mode 100644 index 00000000000..b6a9d4c2683 --- /dev/null +++ b/src/jrd/json/path/btyacc_json.ske @@ -0,0 +1,743 @@ +/* The banner used here should be replaced with an #ident directive */ +/* if the target C compiler supports #ident directives. */ +/* */ +/* If the skeleton is changed, the banner should be changed so that */ +/* the altered version can easily be distinguished from the original. */ + +%% banner + +// +// @(#)btyaccpar, based on byacc 1.8 (Berkeley) +// +#define YYBTYACC 1 + +#include "firebird.h" + +#include +#include +#include + +#include "../jrd/json/JsonRuntime.h" +#include "../jrd/json/path/JsonPath.h" +#include "../jrd/json/path/JPathParser.h" + +#include "../gen/jparse.h" + +%% tables + +#define _C_ "C" + +extern _C_ Yshort yylhs[]; +extern _C_ Yshort yylen[]; +extern _C_ Yshort yydefred[]; +extern _C_ Yshort yydgoto[]; +extern _C_ Yshort yysindex[]; +extern _C_ Yshort yyrindex[]; +extern _C_ Yshort yycindex[]; +extern _C_ Yshort yygindex[]; +extern _C_ Yshort yytable[]; +extern _C_ Yshort yycheck[]; +extern _C_ Yshort yyctable[]; + +#if YYDEBUG +extern _C_ const char *yyname[]; +extern _C_ const char *yyrule[]; +#endif + +%% header + +#ifdef YYREDUCEPOSNFUNC +#define YYCALLREDUCEPOSN(e) \ + if (reduce_posn) { \ + YYREDUCEPOSNFUNC(yyps->pos, &(yyps->psp)[1 - yym], &(yyps->vsp)[1 - yym], \ + yym, yyps->psp - yyps->ps, yychar, yyposn, e); \ + reduce_posn = 0; \ + } + +#ifndef YYCALLREDUCEPOSNARG +#define YYCALLREDUCEPOSNARG yyps->val +#endif + + +#define YYPOSNARG(n) ((yyps->psp)[1 - yym + (n) - 1]) +#define YYPOSNOUT (yyps->pos) +#endif + +// If delete function is not defined by the user, do not deletions. +#ifndef YYDELETEVAL +#define YYDELETEVAL(v, n) +#endif + +// If delete function is not defined by the user, do not deletions. +#ifndef YYDELETEPOSN +#define YYDELETEPOSN(v, n) +#endif + +#define yyclearin (yychar = (-1)) + +#define yyerrok (yyps->errflag = 0) + +#ifndef YYSTACKGROWTH +#define YYSTACKGROWTH 128 +#endif + +#ifndef YYDEFSTACKSIZE +#define YYDEFSTACKSIZE 12 +#endif + + +#define YYABORT goto yyabort +#define YYACCEPT goto yyaccept +#define YYVALID do { if (yyps->save) goto yyvalid; } while(0) +#define YYVALID_NESTED do { if (yyps->save && \ + yyps->save->save == 0) goto yyvalid; } while(0) +// +// For use in generated program +// +#define yytrial (yyps->save) +#define yyvsp (yyps->vsp) +#define yyval (yyps->val) +#define yydepth (yyps->ssp - yyps->ss) + + +%% body + + +void PathParser::yySCopy(YYSTYPE* to, YYSTYPE* from, int size) +{ + int i; + for (i = size - 1; i >= 0; i--) { + to[i] = from[i]; + } +} + + +void PathParser::yyPCopy(YYPOSN* to, YYPOSN* from, int size) +{ + int i; + for (i = size - 1; i >= 0; i--) { + to[i] = from[i]; + } +} + + +void PathParser::yyMoreStack(yyparsestate* yyps) +{ + int p = yyps->ssp - yyps->ss; + Yshort *tss = yyps->ss; + YYSTYPE *tvs = yyps->vs; + YYPOSN *tps = yyps->ps; + yyps->ss = FB_NEW_POOL(getPool()) Yshort [yyps->stacksize + YYSTACKGROWTH]; + yyps->vs = FB_NEW_POOL(getPool()) YYSTYPE[yyps->stacksize + YYSTACKGROWTH]; + yyps->ps = FB_NEW_POOL(getPool()) YYPOSN [yyps->stacksize + YYSTACKGROWTH]; + memcpy(yyps->ss, tss, yyps->stacksize * sizeof(Yshort)); + yySCopy(yyps->vs, tvs, yyps->stacksize); + yyPCopy(yyps->ps, tps, yyps->stacksize); + yyps->stacksize += YYSTACKGROWTH; + delete[] tss; + delete[] tvs; + delete[] tps; + yyps->ssp = yyps->ss + p; + yyps->vsp = yyps->vs + p; + yyps->psp = yyps->ps + p; +} + + +PathParser::yyparsestate* PathParser::yyNewState(int size) +{ + yyparsestate *p = FB_NEW_POOL(getPool()) yyparsestate; + p->stacksize = size + 4; + p->ss = FB_NEW_POOL(getPool()) Yshort [size + 4]; + p->vs = FB_NEW_POOL(getPool()) YYSTYPE[size + 4]; + p->ps = FB_NEW_POOL(getPool()) YYPOSN [size + 4]; + memset(&p->vs[0], 0, (size + 4) * sizeof(YYSTYPE)); + memset(&p->ps[0], 0, (size + 4) * sizeof(YYPOSN)); + return p; +} + + +void PathParser::yyFreeState(PathParser::yyparsestate* p) +{ + delete[] p->ss; + delete[] p->vs; + delete[] p->ps; + delete p; +} + + +int PathParser::parseAux() +{ + // ASF: yym moved to JPathParser.h + int /*yym, */yyn, yystate, yychar; + yyparsestate *yyerrctx = NULL; + int reduce_posn; + +#if YYDEBUG + const char *yys; + + if ((yys = getenv("YYDEBUG"))) { + yyn = *yys; + if (yyn >= '0' && yyn <= '9') + yydebug = yyn - '0'; + } +#endif + + yyps = yyNewState(YYDEFSTACKSIZE); + yyps->save = 0; + yynerrs = 0; + yyps->errflag = 0; + yychar = (-1); + + yyps->ssp = yyps->ss; + yyps->vsp = yyps->vs; + yyps->psp = yyps->ps; + *(yyps->ssp) = yystate = 0; + + + // + // Main parsing loop + // + yyloop: + if ((yyn = yydefred[yystate])) { + goto yyreduce; + } + + // + // Read one token + // + if (yychar < 0) { + if ((yychar = yylex1()) < 0) yychar = 0; +#if YYDEBUG + if (yydebug) { + yys = 0; + if (yychar <= YYMAXTOKEN) yys = yyname[yychar]; + if (!yys) yys = "illegal-symbol"; + printf("yydebug[%d,%p]: state %d, reading %d (%s)", + int(yydepth), yytrial, yystate, yychar, yys); +#ifdef YYDBPR + printf("<"); + YYDBPR(yylval); + printf(">"); +#endif + printf("\n"); + } +#endif + } + + // + // Do we have a conflict? + // + if ((yyn = yycindex[yystate]) && + (yyn += yychar) >= 0 && + yyn <= YYTABLESIZE && + yycheck[yyn] == yychar) { + int ctry; + + if (yypath) { +#if YYDEBUG + if (yydebug) { + printf("yydebug[%d,%p]: CONFLICT in state %d: following successful trial parse\n", + int(yydepth), yytrial, yystate); + } +#endif + // Switch to the next conflict context + yyparsestate *save = yypath; + yypath = save->save; + ctry = save->ctry; + if (save->state != yystate) + goto yyabort; + yyFreeState(save); + + } else { + +#if YYDEBUG + if (yydebug) { + printf("yydebug[%d,%p]: CONFLICT in state %d. ", + int(yydepth), yytrial, yystate); + if (yyps->save) { + printf("ALREADY in conflict. Continue trial parse."); + } else { + printf("Start trial parse."); + } + printf("\n"); + } +#endif + yyparsestate *save = yyNewState(yyps->ssp - yyps->ss); + save->save = yyps->save; + save->state = yystate; + save->errflag = yyps->errflag; + save->ssp = save->ss + (yyps->ssp - yyps->ss); + save->vsp = save->vs + (yyps->vsp - yyps->vs); + save->psp = save->ps + (yyps->psp - yyps->ps); + memcpy (save->ss, yyps->ss, (yyps->ssp - yyps->ss + 1) * sizeof(Yshort)); + yySCopy(save->vs, yyps->vs, (yyps->ssp - yyps->ss + 1)); + yyPCopy(save->ps, yyps->ps, (yyps->ssp - yyps->ss + 1)); + ctry = yytable[yyn]; + if (yyctable[ctry] == -1) { +#if YYDEBUG + if (yydebug && yychar >= 0) + printf("yydebug[%p]: backtracking 1 token\n", yytrial); +#endif + ctry++; + } + save->ctry = ctry; + if (!yyps->save) { + // If this is a first conflict in the stack, start saving lexemes + if (!yylexemes) { + yylexemes = FB_NEW_POOL(getPool()) Yshort[YYSTACKGROWTH]; + yylvals = FB_NEW_POOL(getPool()) YYSTYPE[YYSTACKGROWTH]; + yylvlim = yylvals + YYSTACKGROWTH; + yylpsns = FB_NEW_POOL(getPool()) YYPOSN[YYSTACKGROWTH]; + yylplim = yylpsns + YYSTACKGROWTH; + } + if (yylvp == yylve) { + yylvp = yylve = yylvals; + yylpp = yylpe = yylpsns; + yylexp = yylexemes; + if (yychar >= 0) { + *yylve++ = yylval; + *yylpe++ = yyposn; + *yylexp = yychar; + yychar = -1; + } + } + } + if (yychar >= 0) { + yylvp--, yylpp--, yylexp--; + yychar = -1; + } + save->lexeme = yylvp - yylvals; + yyps->save = save; + } + if (yytable[yyn] == ctry) { +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: state %d, shifting to state %d\n", + int(yydepth), yytrial, yystate, yyctable[ctry]); +#endif + if (yychar < 0) + yylvp++, yylpp++, yylexp++; + yychar = -1; + if (yyps->errflag > 0) --yyps->errflag; + yystate = yyctable[ctry]; + goto yyshift; + } else { + yyn = yyctable[ctry]; + goto yyreduce; + } + } + + // + // Is action a shift? + // + if ((yyn = yysindex[yystate]) && + (yyn += yychar) >= 0 && + yyn <= YYTABLESIZE && + yycheck[yyn] == yychar) { +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: state %d, shifting to state %d\n", + int(yydepth), yytrial, yystate, yytable[yyn]); +#endif + yychar = (-1); + if (yyps->errflag > 0) --yyps->errflag; + yystate = yytable[yyn]; + yyshift: + if (yyps->ssp >= yyps->ss + yyps->stacksize - 1) { + yyMoreStack(yyps); + } + *++(yyps->ssp) = yystate; + *++(yyps->vsp) = yylval; + *++(yyps->psp) = yyposn; + goto yyloop; + } + if ((yyn = yyrindex[yystate]) && + (yyn += yychar) >= 0 && + yyn <= YYTABLESIZE && + yycheck[yyn] == yychar) { + yyn = yytable[yyn]; + goto yyreduce; + } + + // + // Action: error + // + if (yyps->errflag) goto yyinrecovery; + while (yyps->save) { + int ctry; + yyparsestate *save = yyps->save; +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: ERROR in state %d, CONFLICT BACKTRACKING to state %d, %d tokens\n", + int(yydepth), yytrial, yystate, yyps->save->state, int(yylvp - yylvals - yyps->save->lexeme)); +#endif + // Memorize most forward-looking error state in case + // it's really an error. + if (yyerrctx == NULL || yyerrctx->lexeme < yylvp-yylvals) { + // Free old saved error context state + if (yyerrctx) yyFreeState(yyerrctx); + // Create and fill out new saved error context state + yyerrctx = yyNewState(yyps->ssp - yyps->ss); + yyerrctx->save = yyps->save; + yyerrctx->state = yystate; + yyerrctx->errflag = yyps->errflag; + yyerrctx->ssp = yyerrctx->ss + (yyps->ssp - yyps->ss); + yyerrctx->vsp = yyerrctx->vs + (yyps->vsp - yyps->vs); + yyerrctx->psp = yyerrctx->ps + (yyps->psp - yyps->ps); + memcpy (yyerrctx->ss, yyps->ss, (yyps->ssp - yyps->ss + 1) * sizeof(Yshort)); + yySCopy(yyerrctx->vs, yyps->vs, (yyps->ssp - yyps->ss + 1)); + yyPCopy(yyerrctx->ps, yyps->ps, (yyps->ssp - yyps->ss + 1)); + yyerrctx->lexeme = yylvp - yylvals; + } + yylvp = yylvals + save->lexeme; + yylpp = yylpsns + save->lexeme; + yylexp = yylexemes + save->lexeme; + yychar = -1; + yyps->ssp = yyps->ss + (save->ssp - save->ss); + yyps->vsp = yyps->vs + (save->vsp - save->vs); + yyps->psp = yyps->ps + (save->psp - save->ps); + memcpy (yyps->ss, save->ss, (yyps->ssp - yyps->ss + 1) * sizeof(Yshort)); + yySCopy(yyps->vs, save->vs, yyps->vsp - yyps->vs + 1); + yyPCopy(yyps->ps, save->ps, yyps->psp - yyps->ps + 1); + ctry = ++save->ctry; + yystate = save->state; + // We tried shift, try reduce now + if ((yyn = yyctable[ctry]) >= 0) { + goto yyreduce; + } + yyps->save = save->save; + yyFreeState(save); + // + // Nothing left on the stack -- error + // + if (!yyps->save) { +#if YYDEBUG + if (yydebug) { + printf("yydebug[%p]: trial parse FAILED, entering ERROR mode\n", + yytrial); + } +#endif + // Restore state as it was in the most forward-advanced error + yylvp = yylvals + yyerrctx->lexeme; + yylpp = yylpsns + yyerrctx->lexeme; + yylexp = yylexemes + yyerrctx->lexeme; + yychar = yylexp[-1]; + yylval = yylvp[-1]; + yyposn = yylpp[-1]; + yyps->ssp = yyps->ss + (yyerrctx->ssp - yyerrctx->ss); + yyps->vsp = yyps->vs + (yyerrctx->vsp - yyerrctx->vs); + yyps->psp = yyps->ps + (yyerrctx->psp - yyerrctx->ps); + memcpy (yyps->ss, yyerrctx->ss, (yyps->ssp - yyps->ss + 1) * sizeof(Yshort)); + yySCopy(yyps->vs, yyerrctx->vs, yyps->vsp - yyps->vs + 1); + yyPCopy(yyps->ps, yyerrctx->ps, yyps->psp - yyps->ps + 1); + yystate = yyerrctx->state; + yyFreeState(yyerrctx); + yyerrctx = NULL; + } + } + yyerror_detailed("syntax error", yychar, yylval, yyposn); + ++yynerrs; + + yyinrecovery: + if (yyps->errflag < 3) { + yyps->errflag = 3; + for (;;) { + if ((yyn = yysindex[*(yyps->ssp)]) && + (yyn += YYERRCODE) >= 0 && + yyn <= YYTABLESIZE && + yycheck[yyn] == YYERRCODE) { +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: state %d, ERROR recovery shifts to state %d\n", + int(yydepth), yytrial, *(yyps->ssp), yytable[yyn]); +#endif + yystate = yytable[yyn]; + goto yyshift; + } else { +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: ERROR recovery discards state %d\n", + int(yydepth), yytrial, *(yyps->ssp)); +#endif + if (yyps->ssp <= yyps->ss) { + goto yyabort; + } + if (!yytrial) { + YYDELETEVAL(yyps->vsp[0], 1); + YYDELETEPOSN(yyps->psp[0], 1); + } + --(yyps->ssp); + --(yyps->vsp); + --(yyps->psp); + } + } + } else { + if (yychar == 0) goto yyabort; +#if YYDEBUG + if (yydebug) { + yys = 0; + if (yychar <= YYMAXTOKEN) yys = yyname[yychar]; + if (!yys) yys = "illegal-symbol"; + printf("yydebug[%d,%p]: state %d, ERROR recovery discards token %d (%s)\n", + int(yydepth), yytrial, yystate, yychar, yys); + } +#endif + if (!yytrial) { + YYDELETEVAL(yylval, 0); + YYDELETEPOSN(yyposn, 0); + } + yychar = (-1); + goto yyloop; + } + + // + // Reduce the rule + // +yyreduce: + yym = yylen[yyn]; +#if YYDEBUG + if (yydebug) { + printf("yydebug[%d,%p]: state %d, reducing by rule %d (%s)", + int(yydepth), yytrial, yystate, yyn, yyrule[yyn]); +#ifdef YYDBPR + if (yym) { + int i; + printf("<"); + for (i = yym; i > 0; i--) { + if (i != yym) printf(", "); + YYDBPR((yyps->vsp)[1 - i]); + } + printf(">"); + } +#endif + printf("\n"); + } +#endif + if (yyps->ssp + 1 - yym >= yyps->ss + yyps->stacksize) { + yyMoreStack(yyps); + } + + /* default action - assign last argument as in standard yacc */ + yyps->val = yyvsp[1 - yym]; + + /* default reduced position is NULL -- no position at all. + no position will be assigned at trial time and if no position handling is present */ + memset(&yyps->pos, 0, sizeof(yyps->pos)); + + reduce_posn = TRUE; + + switch (yyn) { + +%% trailer + + default: + break; + } + +#if YYDEBUG && defined(YYDBPR) + if (yydebug) { + printf("yydebug[%d]: after reduction, result is ", yytrial); + YYDBPR(yyps->val); + printf("\n"); + } +#endif + + // Perform user-defined position reduction +#ifdef YYREDUCEPOSNFUNC + if (!yytrial) { + YYCALLREDUCEPOSN(YYREDUCEPOSNFUNCARG); + } +#endif + + yyps->ssp -= yym; + yystate = *(yyps->ssp); + yyps->vsp -= yym; + yyps->psp -= yym; + + yym = yylhs[yyn]; + if (yystate == 0 && yym == 0) { +#if YYDEBUG + if (yydebug) { + printf("yydebug[%d,%p]: after reduction, shifting from state 0 to state %d\n", + int(yydepth), yytrial, YYFINAL); + } +#endif + yystate = YYFINAL; + *++(yyps->ssp) = YYFINAL; + *++(yyps->vsp) = yyps->val; + yyretlval = yyps->val; // return value of root non-terminal to yylval + *++(yyps->psp) = yyps->pos; + yyretposn = yyps->pos; // return value of root position to yyposn + if (yychar < 0) { + if ((yychar = yylex1()) < 0) { + yychar = 0; + } +#if YYDEBUG + if (yydebug) { + yys = 0; + if (yychar <= YYMAXTOKEN) yys = yyname[yychar]; + if (!yys) yys = "illegal-symbol"; + printf("yydebug[%d,%p]: state %d, reading %d (%s)\n", + int(yydepth), yytrial, YYFINAL, yychar, yys); + } +#endif + } + if (yychar == 0) goto yyaccept; + goto yyloop; + } + + if ((yyn = yygindex[yym]) && (yyn += yystate) >= 0 && + yyn <= YYTABLESIZE && yycheck[yyn] == yystate) { + yystate = yytable[yyn]; + } else { + yystate = yydgoto[yym]; + } +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: after reduction, shifting from state %d to state %d\n", + int(yydepth), yytrial, *(yyps->ssp), yystate); +#endif + if (yyps->ssp >= yyps->ss + yyps->stacksize - 1) { + yyMoreStack(yyps); + } + *++(yyps->ssp) = yystate; + *++(yyps->vsp) = yyps->val; + *++(yyps->psp) = yyps->pos; + goto yyloop; + + + // + // Reduction declares that this path is valid. + // Set yypath and do a full parse + // +yyvalid: + if (yypath) { + goto yyabort; + } + while (yyps->save) { + yyparsestate *save = yyps->save; + yyps->save = save->save; + save->save = yypath; + yypath = save; + } +#if YYDEBUG + if (yydebug) + printf("yydebug[%d,%p]: CONFLICT trial successful, backtracking to state %d, %d tokens\n", + int(yydepth), yytrial, yypath->state, int(yylvp - yylvals - yypath->lexeme)); +#endif + if (yyerrctx) { + yyFreeState(yyerrctx); + yyerrctx = NULL; + } + yychar = -1; + yyps->ssp = yyps->ss + (yypath->ssp - yypath->ss); + yyps->vsp = yyps->vs + (yypath->vsp - yypath->vs); + yyps->psp = yyps->ps + (yypath->psp - yypath->ps); + memcpy (yyps->ss, yypath->ss, (yyps->ssp - yyps->ss + 1) * sizeof(Yshort)); + yySCopy(yyps->vs, yypath->vs, yyps->vsp - yyps->vs + 1); + yyPCopy(yyps->ps, yypath->ps, yyps->psp - yyps->ps + 1); + yylvp = yylvals + yypath->lexeme; + yylpp = yylpsns + yypath->lexeme; + yylexp = yylexemes + yypath->lexeme; + yystate = yypath->state; + goto yyloop; + + +yyabort: + if (yyerrctx) { + yyFreeState(yyerrctx); + yyerrctx = NULL; + } + + YYSTYPE *pv; + for(pv = yyps->vs; pv < yyps->vsp; pv++) { + YYDELETEVAL(*pv, 2); + } + + YYPOSN *pp; + for(pp = yyps->ps; pp < yyps->psp; pp++) { + YYDELETEPOSN(*pp, 2); + } + + while (yyps) { + yyparsestate *save = yyps; + yyps = save->save; + yyFreeState(save); + } + while (yypath) { + yyparsestate *save = yypath; + yypath = save->save; + yyFreeState(save); + } + return (1); + + +yyaccept: + if (yyps->save) goto yyvalid; + if (yyerrctx) { + yyFreeState(yyerrctx); + yyerrctx = NULL; + } + while (yyps) { + yyparsestate *save = yyps; + yyps = save->save; + yyFreeState(save); + } + while (yypath) { + yyparsestate *save = yypath; + yypath = save->save; + yyFreeState(save); + } + return (0); +} + + +int PathParser::yylex1() +{ + if (yylvp < yylve) { + yylval = *yylvp++; + yyposn = *yylpp++; + return *yylexp++; + } else { + if (yyps->save) { + if (yylvp == yylvlim) { + yyexpand(); + } + *yylexp = yylex(); + *yylvp++ = yylval; + yylve++; + *yylpp++ = yyposn; + yylpe++; + return *yylexp++; + } else { + return yylex(); + } + } +} + + +int PathParser::yyexpand() +{ + int p = yylvp - yylvals; + int s = yylvlim - yylvals; + s += YYSTACKGROWTH; + { Yshort *tl = yylexemes; + YYSTYPE *tv = yylvals; + YYPOSN *tp = yylpsns; + yylvals = FB_NEW_POOL(getPool()) YYSTYPE[s]; + yylpsns = FB_NEW_POOL(getPool()) YYPOSN[s]; + yylexemes = FB_NEW_POOL(getPool()) Yshort[s]; + memcpy(yylexemes, tl, (s - YYSTACKGROWTH) * sizeof(Yshort)); + yySCopy(yylvals, tv, s - YYSTACKGROWTH); + yyPCopy(yylpsns, tp, s - YYSTACKGROWTH); + delete[] tl; + delete[] tv; + delete[] tp; + } + yylvp = yylve = yylvals + p; + yylvlim = yylvals + s; + yylpp = yylpe = yylpsns + p; + yylplim = yylpsns + s; + yylexp = yylexemes + p; + return 0; +} diff --git a/src/jrd/json/path/jpath-parse-conflicts.txt b/src/jrd/json/path/jpath-parse-conflicts.txt new file mode 100644 index 00000000000..94a2adeddb7 --- /dev/null +++ b/src/jrd/json/path/jpath-parse-conflicts.txt @@ -0,0 +1 @@ +4 shift/reduce conflicts, 3 reduce/reduce conflicts. diff --git a/src/jrd/tests/TestContext.cpp b/src/jrd/tests/TestContext.cpp new file mode 100644 index 00000000000..1d3525d12c6 --- /dev/null +++ b/src/jrd/tests/TestContext.cpp @@ -0,0 +1,81 @@ +#include "TestContext.h" +#include "../jrd/req.h" + +#include "../jrd/trace/TraceManager.h" + +#include "boost/test/unit_test.hpp" + +#include + +// It is to slow to create a new DB for each test, so create it once and keep in cache +class CachedAttach +{ +public: + CachedAttach(Firebird::MemoryPool& pool) : + status(pool) + { + dbPath = std::filesystem::temp_directory_path() / "engine_utests.fdb"; + removeDb(); + } + + Jrd::JAttachment* getAttachment() + { + // Delay init + if (att != nullptr) + return att; + + att = prov.createDatabase(&status, dbPath.string().data(), 0, nullptr); + BOOST_REQUIRE(att); + + tra = att->startTransaction(&status, 0, nullptr); + BOOST_REQUIRE(tra); + + statement = att->prepare(&status, tra, 0, "select 1 from rdb$database", 3, 0); + + return att; + } + + ~CachedAttach() + { + att->release(); + Jrd::TraceManager::getStorage()->shutdown(); + removeDb(); + } + + void removeDb() + { + if (std::filesystem::exists(dbPath)) + std::filesystem::remove(dbPath); + } + + Jrd::JProvider prov{nullptr}; + Firebird::FbLocalStatus status; + + + std::filesystem::path dbPath; + Jrd::JAttachment* att = nullptr; + + Jrd::JTransaction* tra = nullptr; + Jrd::JStatement* statement = nullptr; +}; +static Firebird::GlobalPtr storage; + + +TestContextHolder::TestContextHolder() : + m_tdbb(&storage->status, storage->getAttachment(), FB_FUNCTION), + pool(*getDefaultMemoryPool()) +{ + m_tdbb->setRequest(storage->statement->getHandle()->getRequest()); + m_tdbb->setTransaction(storage->tra->getHandle()); + + ISC_TIMESTAMP ts = {}; + m_tdbb->getRequest()->setGmtTimeStamp(ts); + + tdbb = JRD_get_thread_data(); +} + +TestContextHolder::~TestContextHolder() +{ + m_tdbb->setRequest(nullptr); + m_tdbb->setTransaction(nullptr); +} diff --git a/src/jrd/tests/TestContext.h b/src/jrd/tests/TestContext.h new file mode 100644 index 00000000000..b2c5050208a --- /dev/null +++ b/src/jrd/tests/TestContext.h @@ -0,0 +1,23 @@ +#ifndef TEST_CONTEXT_H +#define TEST_CONTEXT_H + +#include "firebird.h" +#include "../jrd/jrd.h" + +class TestContextHolder +{ +public: + TestContextHolder(); + ~TestContextHolder(); + +private: + Jrd::EngineContextHolder m_tdbb; // must be at stack + +public: // Available in tests + Firebird::MemoryPool& pool; + Jrd::thread_db* tdbb{}; +}; + +using EngineHolder = TestContextHolder; + +#endif // TEST_CONTEXT_H diff --git a/src/jrd/tests/json/DecimalConvert.cpp b/src/jrd/tests/json/DecimalConvert.cpp new file mode 100644 index 00000000000..2364381f07a --- /dev/null +++ b/src/jrd/tests/json/DecimalConvert.cpp @@ -0,0 +1,114 @@ +#include "firebird.h" +#include "boost/test/unit_test.hpp" +#include "../jrd/json/JsonUtils.h" +#include "JsonTestUtils.h" + +using namespace Firebird; + + +class LocaleChangeHolder +{ +public: + LocaleChangeHolder(const char* newLocale) + { + m_prevLocale = std::setlocale(LC_NUMERIC, nullptr); + BOOST_ASSERT(m_prevLocale != nullptr); + + const char* loc = std::setlocale(LC_NUMERIC, newLocale); + BOOST_ASSERT(loc != nullptr); + } + + ~LocaleChangeHolder() + { + const char* loc = std::setlocale(LC_NUMERIC, m_prevLocale); + BOOST_ASSERT(loc != nullptr); + } + +private: + char* m_prevLocale; +}; + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(ConvertTests) + +BOOST_AUTO_TEST_SUITE(FractionalTest) + +constexpr double NUMBER_TO_CONVERT = 3.1415926535'8979323846'2643383279'5028841971'6939937510; + + +BOOST_AUTO_TEST_CASE(ruUtf8LocaleTest) +{ + SKIP_TEST; // Missing locale + LocaleChangeHolder ruEncoding("ru_RU.UTF-8"); + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, NUMBER_TO_CONVERT).find('.') != std::string_view::npos); +} + +BOOST_AUTO_TEST_CASE(enUtf8LocaleTest) +{ + LocaleChangeHolder ruEncoding("en_US.UTF-8"); + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, NUMBER_TO_CONVERT).find('.') != std::string_view::npos); +} + +BOOST_AUTO_TEST_CASE(russianLocaleTest) +{ + SKIP_TEST // Missing locale + LocaleChangeHolder ruEncoding("russian"); + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, NUMBER_TO_CONVERT).find('.') != std::string_view::npos); +} + +BOOST_AUTO_TEST_SUITE_END() // FractionalTest + + +BOOST_AUTO_TEST_SUITE(TypesTest) + +BOOST_AUTO_TEST_CASE(floatTest) +{ + const float floatNumber = 3.14151f; + std::string_view expectedString = "3.14151"; + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, floatNumber).substr(0, expectedString.length()) == "3.14151"); +} + +BOOST_AUTO_TEST_SUITE_END() // FractionalTest + +BOOST_AUTO_TEST_CASE(doubleTest) +{ + const double doubleNumber = 3.1415926; + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, doubleNumber) == "3.1415926"); +} + +BOOST_AUTO_TEST_CASE(intTest) +{ + const int32_t intNumber = 2147483646; + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, intNumber) == "2147483646"); +} + + +BOOST_AUTO_TEST_CASE(NegativeintTest) +{ + const int32_t intNumber = -2147483646; + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, intNumber) == "-2147483646"); +} +BOOST_AUTO_TEST_CASE(bigintTest) +{ + const u_int64_t bigintNumber = 9223372037; + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, bigintNumber) == "9223372037"); +} + +BOOST_AUTO_TEST_SUITE_END() // ConvertTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite diff --git a/src/jrd/tests/json/JsonExprNodeTest.cpp b/src/jrd/tests/json/JsonExprNodeTest.cpp new file mode 100644 index 00000000000..62f6049eb44 --- /dev/null +++ b/src/jrd/tests/json/JsonExprNodeTest.cpp @@ -0,0 +1,793 @@ +#include "boost/test/unit_test.hpp" +#include "../jrd/json/JsonRuntime.h" +#include "../jrd/json/path/JsonPath.h" + +#include "../TestContext.h" + +using namespace FBJSON; + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(JsonNodesTests) + +BOOST_AUTO_TEST_SUITE(FilterNodeTests) +BOOST_AUTO_TEST_SUITE(ChildrenRoutinesTests) + +BOOST_AUTO_TEST_CASE(CalculationNodeTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + { // Omit + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + const bool added = root.addChild(new JsonExprNode(pool, JsonExprNode::CALCULATION_NODE)); + + BOOST_TEST(added == false); + BOOST_CHECK(root.getChildrenCount() == 0); + } + + { // keep + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + const bool added = root.addChild(new JsonExprNode(pool, JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID)); + + BOOST_TEST(added == true); + BOOST_CHECK(root.getChildrenCount() == 1); + } + + { // keep + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + const bool added = root.addChild(new JsonExprNode(pool, JsonExprNode::FILTER_NODE, JsonExprNode::FLAG_SOLID)); + + BOOST_TEST(added == true); + BOOST_CHECK(root.getChildrenCount() == 1); + } + + { // keep + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + const bool added = root.addChild(new JsonExprNode(pool, JsonExprNode::METHOD_NODE, JsonExprNode::FLAG_SOLID)); + + BOOST_TEST(added == true); + BOOST_CHECK(root.getChildrenCount() == 1); + } + + { // keep + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + const bool added = root.addChild(new JsonExprNode(pool, JsonExprNode::VARIABLE_NODE, JsonExprNode::FLAG_SOLID)); + + BOOST_TEST(added == true); + BOOST_CHECK(root.getChildrenCount() == 1); + } + + { // keep + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + const bool added = root.addChild(new JsonExprNode(pool, JsonExprNode::SCALAR_NODE, JsonExprNode::FLAG_SOLID)); + + BOOST_TEST(added == true); + BOOST_CHECK(root.getChildrenCount() == 1); + } + + { + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + auto child = root.addChild(JsonExprNode::SCALAR_NODE); + + child->testType(JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(root.getChildrenCount() == 1); + BOOST_TEST(root.getFirstChild() == child); + } + + { + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + JsonExprNode* child = root.addChild(JsonExprNode::SCALAR_NODE); + + BOOST_CHECK(root.getChildrenCount() == 1); + BOOST_TEST(root.getLastChild() == child); + } + + { // Merge + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + JsonExprNode* child = new JsonExprNode(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + child->addChild(JsonExprNode::SCALAR_NODE); + child->addOperation(JsonExprOperation::ADDITION); + child->addChild(JsonExprNode::SCALAR_NODE); + child->finish(); + root.addChild(child); // merge + + BOOST_CHECK(root.getChildrenCount() == 2); + BOOST_TEST(root.getFirstChild() != child); + BOOST_TEST(root.getLastChild() != child); + } + + { // no Merge + JsonExprNode root(pool, JsonExprNode::CALCULATION_NODE); + JsonExprNode* child = new JsonExprNode(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + child->addChild(JsonExprNode::SCALAR_NODE); + child->addOperation(JsonExprOperation::ADDITION); + child->addChild(JsonExprNode::SCALAR_NODE); + child->addFlag(JsonExprNode::FLAG_SOLID); + child->finish(); + root.addChild(child); // no merge + + BOOST_TEST(root.getChildrenCount() == ULONG(1)); + BOOST_TEST(root.getFirstChild() == child); + } + + { // Operation + JsonExprNode root(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + // OK + for (uint32_t i = 0; i < JsonExprNode::OPERATIONS_LIMIT; ++i) + { + root.addOperation(JsonExprOperation::DIVISION); + } + + // Limit + BOOST_CHECK_THROW(root.addOperation(JsonExprOperation::DIVISION), json_syntax_exception); + } + + { // Variable + JsonExprNode root(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + + PathVariable* var = new PathVariable(pool); + JsonExprNode* child = root.addVariable(var); + BOOST_TEST(&root != child); + BOOST_CHECK(child->isVariable()); + BOOST_TEST(child->getVariable() == var); + } + + { // makeHeadNode + JsonExprNode root(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + root.makeHeadNode(true); + + BOOST_CHECK(root.getLastChild()->isVariable()); + BOOST_CHECK(root.getLastChild()->getVariable()->type == PathVariable::Type::HEAD); + + auto rootPath = root.getLastChild()->getVariable()->path->getRootNode(); + BOOST_CHECK((rootPath->flags & PathNode::FLAG_UNWRAP) != 0); + } + + { // makeHeadNode + JsonExprNode root(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + root.makeHeadNode(false); + + BOOST_TEST(root.getChildrenCount() == ULONG(1)); + BOOST_CHECK(root.getLastChild()->isVariable()); + BOOST_CHECK(root.getLastChild()->getVariable()->type == PathVariable::Type::HEAD); + BOOST_TEST((root.getLastChild()->getVariable()->path->getRootNode()->flags & PathNode::FLAG_UNWRAP) == false); + } + + { // makeHeadNode + JsonExprNode root(pool, PathMethod::ABS); + root.makeHeadNode(false); + + BOOST_TEST(root.getChildrenCount() == ULONG(1)); + BOOST_CHECK(root.getFirstChild()->isVariable()); + BOOST_CHECK(root.getFirstChild()->getVariable()->type == PathVariable::Type::HEAD); + BOOST_CHECK((root.getFirstChild()->getVariable()->path->getRootNode()->flags & PathNode::FLAG_UNWRAP) == false); + } + + { // setHead + JsonExprNode root(pool, PathMethod::ABS); + + JsonExprNode* child = new JsonExprNode(pool, JsonExprNode::SCALAR_NODE); + + root.setHead(child); + + BOOST_TEST(root.getChildrenCount() == ULONG(1)); + BOOST_CHECK(root.getFirstChild() == child); + } + + { // setPathMethod + JsonExprNode root(pool, PathMethod::CEILING); + + BOOST_TEST(root.getChildrenCount() == ULONG(1)); + BOOST_CHECK(root.getFirstChild() == nullptr); + BOOST_CHECK(root.getPathMethod() == PathMethod::CEILING); + } + + { // finish keep + JsonExprNode* root = new JsonExprNode(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + root->addChild(JsonExprNode::SCALAR_NODE); + root->addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(root->canBeOmitted() == false); + + auto parent = root->finish(); + + BOOST_REQUIRE(parent == root); + + delete root; + } + + { // finish omit + const auto test = 123; + + JsonExprNode* root = new JsonExprNode(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + auto child = root->addChild(JsonExprNode::SCALAR_NODE); + child->set(test); + BOOST_TEST(root->canBeOmitted()); + + auto parent = root->finish(); + + BOOST_TEST(parent == child); + } +} +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(SettersTests) + +BOOST_AUTO_TEST_CASE(SetVariableNodeTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + PathVariable* var = new PathVariable(pool); + FBJSON::JsonExprNode node(pool, var); + + BOOST_TEST(node.getVariable() == var); + BOOST_TEST(node.testType(JsonExprNode::VARIABLE_NODE)); +} + + +BOOST_AUTO_TEST_CASE(SetScalarTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + ULONG id = 0; + //ContextVariables ctx(id, {}); + { // int + const auto test = 10; + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.set(test); + + BOOST_TEST(node.testType(JsonExprNode::SCALAR_NODE)); + //BOOST_TEST(node.execute(ctx).getValue().integer == 10); + } + + + { // int64 + const auto test = 1000000000; + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.set(test); + + BOOST_TEST(node.testType(JsonExprNode::SCALAR_NODE)); + //BOOST_TEST(node.execute(ctx).getValue().integer == test); + } + + { // double + const auto testValue = 3.14; + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.set(testValue); + + BOOST_TEST(node.testType(JsonExprNode::SCALAR_NODE)); + //BOOST_TEST(node.execute(ctx).getValue().doubleValue == testValue); + } + + { // bool + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.set(true); + + BOOST_TEST(node.testType(JsonExprNode::SCALAR_NODE)); + //BOOST_TEST(node.execute(ctx).getValue().boolean == true); + } + + { // null + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.setNull(); + + BOOST_TEST(node.testType(JsonExprNode::SCALAR_NODE)); + //BOOST_TEST(node.execute(ctx).isNull()); + } + + { // string + const auto testValue = "123"; + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.set(testValue); + + // auto res = node.execute(ctx); + + // BOOST_TEST(node.testType(JsonExprNode::SCALAR_NODE)); + // BOOST_TEST(res.isString()); + // BOOST_TEST(res.getStringView() == testValue); + } +} + +BOOST_AUTO_TEST_SUITE_END() + + +BOOST_AUTO_TEST_SUITE(ChildrenRoutineTest) + + +BOOST_AUTO_TEST_CASE(RoutineTests) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(node.getChildrenCount() == 0); + + auto first = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_CHECK(node.getFirstChild() == first); + BOOST_CHECK(node.getLastChild() == first); + BOOST_CHECK(node.getChildrenCount() == 1); + + auto second = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_CHECK(node.getFirstChild() == first); + BOOST_CHECK(node.getSecondChild() == second); + BOOST_CHECK(node.getLastChild() == second); + BOOST_CHECK(node.getChildrenCount() == 2); + + auto third = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_CHECK(node.getFirstChild() == first); + BOOST_CHECK(node.getSecondChild() == second); + BOOST_CHECK(node.getLastChild() == third); + BOOST_CHECK(node.getChildrenCount() == 3); + + node.clearChildren(); + BOOST_CHECK(node.getChildrenCount() == 0); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(CheckersTests) + +BOOST_AUTO_TEST_CASE(IsEmptyTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + { // Scalar child + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(node.isRootEmpty()); + + node.addChild(JsonExprNode::SCALAR_NODE)->setNull(); + BOOST_CHECK(node.isRootEmpty() == false); + } + + { // Empty children + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(node.isRootEmpty()); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == true); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == false); + } + + { // Empty children + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(node.isRootEmpty()); + + node.addOperation(JsonExprOperation::AND); + BOOST_CHECK(node.isRootEmpty() == false); + } + + + { // Empty children + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::FILTER_NODE); + BOOST_CHECK(node.isRootEmpty()); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == false); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == false); + } + + { // Empty children + FBJSON::JsonExprNode node(pool, PathMethod::ABS); + BOOST_CHECK(node.isRootEmpty() == false); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == false); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == false); + } + + { // Empty children + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::VARIABLE_NODE); + BOOST_CHECK(node.isRootEmpty() == false); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + BOOST_CHECK(node.isRootEmpty() == false); + } + + { + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::FILTER_NODE); + BOOST_CHECK(node.isRootEmpty()); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); // Add calculation + BOOST_CHECK(node.isRootEmpty() == false); + } + + { + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::COMPOUND_NODE); + BOOST_CHECK(node.isRootEmpty() == true); + + node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); // Add calculation + BOOST_CHECK(node.isRootEmpty() == false); + } +} + + +BOOST_AUTO_TEST_CASE(CommonCheckersTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + { // Scalar + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::SCALAR_NODE); + node.set(1); + + BOOST_CHECK(node.isVariable() == false); + BOOST_CHECK(node.testType(JsonExprNode::SCALAR_NODE) == true); + BOOST_CHECK(node.testType(JsonExprNode::CALCULATION_NODE) == false); + BOOST_CHECK(node.canHasTail() == false); + BOOST_CHECK(node.getTailNode() == nullptr); + BOOST_CHECK(node.canBeOmitted() == false); + } + + { // Variable + FBJSON::JsonExprNode node(pool, new PathVariable(pool)); + + BOOST_CHECK(node.isVariable() == true); + BOOST_CHECK(node.testType(JsonExprNode::VARIABLE_NODE) == true); + BOOST_CHECK(node.testType(JsonExprNode::CALCULATION_NODE) == false); + BOOST_CHECK(node.canHasTail() == true); + BOOST_CHECK(node.getTailNode() == nullptr); + BOOST_CHECK(node.canBeOmitted() == false); + } + + { // Calculation + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + + BOOST_CHECK(node.isVariable() == false); + BOOST_CHECK(node.testType(JsonExprNode::CALCULATION_NODE) == true); + BOOST_CHECK(node.testType(JsonExprNode::VARIABLE_NODE) == false); + BOOST_CHECK(node.canHasTail() == false); + BOOST_CHECK(node.getTailNode() == nullptr); + BOOST_CHECK(node.canBeOmitted() == false); + } + + { // Method + FBJSON::JsonExprNode node(pool, PathMethod::DOUBLE); + + BOOST_CHECK(node.isVariable() == false); + BOOST_CHECK(node.testType(JsonExprNode::METHOD_NODE) == true); + BOOST_CHECK(node.testType(JsonExprNode::CALCULATION_NODE) == false); + BOOST_CHECK(node.canHasTail() == true); + BOOST_CHECK(node.getTailNode() == nullptr); + BOOST_CHECK(node.canBeOmitted() == false); + + } + + { // Filter + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::FILTER_NODE); + + BOOST_CHECK(node.isVariable() == false); + BOOST_CHECK(node.testType(JsonExprNode::FILTER_NODE) == true); + BOOST_CHECK(node.testType(JsonExprNode::CALCULATION_NODE) == false); + BOOST_CHECK(node.canHasTail() == true); + BOOST_CHECK(node.getTailNode() == nullptr); + BOOST_CHECK(node.canBeOmitted() == false); + } + + { // Compound + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::COMPOUND_NODE); + + BOOST_CHECK(node.isVariable() == false); + BOOST_CHECK(node.testType(JsonExprNode::COMPOUND_NODE) == true); + BOOST_CHECK(node.testType(JsonExprNode::CALCULATION_NODE) == false); + BOOST_CHECK(node.getTailNode() == nullptr); + BOOST_CHECK(node.canBeOmitted() == false); + } +} + +BOOST_AUTO_TEST_CASE(OmitTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + { // 1 + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(node.canBeOmitted() == false); + node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_CHECK(node.canBeOmitted() == true); + } + + + { // 2 + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + node.addChild(JsonExprNode::SCALAR_NODE); + node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_CHECK(node.canBeOmitted() == false); + } + + { // 3 + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + node.addChild(JsonExprNode::SCALAR_NODE); + node.addOperation(JsonExprOperation::ADDITION); + BOOST_CHECK(node.canBeOmitted() == false); + } +} + +BOOST_AUTO_TEST_CASE(TailTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + { // method + + { + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::METHOD_NODE); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == true); + BOOST_TEST(node.getTailNode() == lastNode); + lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == true); + BOOST_TEST(node.getTailNode() == lastNode); + } + + { + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::VARIABLE_NODE); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == true); + BOOST_TEST(node.getTailNode() == lastNode); + lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + BOOST_TEST(node.hasTail() == true); + + // lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + // BOOST_TEST(node.hasTail() == false); + // lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + // BOOST_TEST(node.hasTail() == false); + // BOOST_TEST(node.getTailNode() == nullptr); + + // lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + // BOOST_TEST(node.hasTail() == false); + // lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + // BOOST_TEST(node.hasTail() == false); + } + + { + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::FILTER_NODE); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + BOOST_TEST(node.hasTail() == false); + + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == false); + BOOST_TEST(node.getTailNode() == nullptr); + lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + BOOST_TEST(node.hasTail() == false); + + lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == true); + BOOST_TEST(node.getTailNode() == lastNode); + lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + BOOST_TEST(node.hasTail() == true); + + // lastNode = node.addChild(JsonExprNode::SCALAR_NODE); + // BOOST_TEST(node.hasTail() == false); + // BOOST_TEST(node.getTailNode() == nullptr); + // lastNode->addFlag(FBJSON::JsonExprNode::FLAG_METHOD_ARGUMENT); + // BOOST_TEST(node.hasTail() == false); + } + } + + + { // variable + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::VARIABLE_NODE); + BOOST_TEST(node.hasTail() == false); + node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == true); + } + + { // Filter + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::FILTER_NODE); + BOOST_TEST(node.hasTail() == false); + node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == false); + node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == false); + node.addChild(JsonExprNode::SCALAR_NODE); + BOOST_TEST(node.hasTail() == true); + } +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(HelpersTests) +BOOST_AUTO_TEST_CASE(HelperTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + { // Has Tail + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::VARIABLE_NODE); + auto* child = node.addChild(FBJSON::JsonExprNode::CALCULATION_NODE, JsonExprNode::FLAG_SOLID); + + BOOST_REQUIRE(node.hasTail()); + node.unwrap(); // Nothing + BOOST_CHECK(child->testType(FBJSON::JsonExprNode::CALCULATION_NODE)); + } + + { // No Tail not variable + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + BOOST_REQUIRE(node.hasTail() == false); + + node.unwrap(); // Nothing + BOOST_CHECK(node.getChildrenCount() == 0); + } + + { // No Tail, empty variable + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::VARIABLE_NODE); + node.unwrap(); // nothing + BOOST_CHECK(node.getChildrenCount() == 0); + BOOST_CHECK(node.getVariable() == nullptr); + } + + { // Variable + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, new PathVariable(pool)); + + BOOST_CHECK(node.getVariable()->path == nullptr); + node.unwrap(); + BOOST_CHECK(node.getVariable()->path != nullptr); + BOOST_CHECK(node.getVariable()->path->getRootNode()->flags & PathNode::FLAG_UNWRAP); + } + + { // Variable with path + FBJSON::JsonExprNode* lastNode = nullptr; + + auto var = new PathVariable(pool); + var->path = new JsonPath(pool); + FBJSON::JsonExprNode node(pool, var); + + BOOST_CHECK(node.getVariable()->path != nullptr); + node.unwrap(); + BOOST_CHECK(node.getVariable()->path == var->path); + BOOST_CHECK(node.getVariable()->path->getRootNode() == var->path->getRootNode()); + BOOST_CHECK(node.getVariable()->path->getRootNode()->flags & PathNode::FLAG_UNWRAP); + } +} + +BOOST_AUTO_TEST_CASE(ScalarHelpersTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + { + FBJSON::JsonExprNode node(pool, JsonExprNode::SCALAR_NODE); + node.set(1); + BOOST_CHECK(node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == -1); // TODO: Get value via execute method + + BOOST_CHECK(node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == 1); + + BOOST_CHECK(node.applyUnaryOp(JsonExprOperation::UNARY_PLUS)); + BOOST_CHECK(node.getRangeNumber() == 1); + + node.set(-42); + BOOST_CHECK(node.applyUnaryOp(JsonExprOperation::UNARY_PLUS)); + BOOST_CHECK(node.getRangeNumber() == -42); + + node.set(42); + BOOST_CHECK(node.applyUnaryOp(JsonExprOperation::UNARY_PLUS)); + BOOST_CHECK(node.getRangeNumber() == 42); + } + + { + FBJSON::JsonExprNode node(pool, JsonExprNode::CALCULATION_NODE); + BOOST_CHECK(!node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == std::nullopt); + } + + { + FBJSON::JsonExprNode node(pool, JsonExprNode::VARIABLE_NODE); + BOOST_CHECK(!node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == std::nullopt); + } + + { + FBJSON::JsonExprNode node(pool, JsonExprNode::METHOD_NODE); + BOOST_CHECK(!node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == std::nullopt); + } + + { + FBJSON::JsonExprNode node(pool, JsonExprNode::FILTER_NODE); + BOOST_CHECK(!node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == std::nullopt); + } + + { + FBJSON::JsonExprNode node(pool, JsonExprNode::COMPOUND_NODE); + BOOST_CHECK(!node.applyUnaryOp(JsonExprOperation::UNARY_MINUS)); + BOOST_CHECK(node.getRangeNumber() == std::nullopt); + } + + { + FBJSON::JsonExprNode node(pool, JsonExprNode::SCALAR_NODE); + node.set(4000000); + BOOST_CHECK_THROW(node.getRangeNumber(), json_fatal_exception); + node.set(-4000000); + BOOST_CHECK_THROW(node.getRangeNumber(), json_fatal_exception); + } +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE(FilterTests) + +BOOST_AUTO_TEST_CASE(FilterTrueFalseTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + const auto big = 10; + const auto low = 5; + { // true + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + node.addChild(JsonExprNode::SCALAR_NODE)->set(big); + node.addOperation(JsonExprOperation::MORE); + node.addChild(JsonExprNode::SCALAR_NODE)->set(low); + node.finish(); + + // ULONG id = 0; + // ContextVariables ctx(id, {}); + // auto res = JsonExprNode::passFilter(ctx, &node); + + // BOOST_CHECK(res == JsonExprNode::FilterResult::RTRUE); + } + + { // false + FBJSON::JsonExprNode* lastNode = nullptr; + + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + node.addChild(JsonExprNode::SCALAR_NODE)->set(big); + node.addOperation(JsonExprOperation::LESS); + node.addChild(JsonExprNode::SCALAR_NODE)->set(low); + node.finish(); + + ULONG id = 0; + // ContextVariables ctx(id, {}); + // auto res = JsonExprNode::passFilter(ctx, &node); + + // BOOST_CHECK(res == JsonExprNode::FilterResult::RFALSE); + } + + { // Unknown + FBJSON::JsonExprNode node(pool, FBJSON::JsonExprNode::CALCULATION_NODE); + node.addChild(JsonExprNode::SCALAR_NODE)->set(big); + node.addOperation(JsonExprOperation::LESS); + node.addChild(JsonExprNode::SCALAR_NODE)->set(""); + node.finish(); + + ULONG id = 0; + // ContextVariables ctx(id, {}); + // auto res = JsonExprNode::passFilter(ctx, &node); + + // BOOST_CHECK(res == JsonExprNode::FilterResult::RUNKNOWN); + } +} + +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE_END() // FilterTests + +BOOST_AUTO_TEST_SUITE_END() // JsonNodesTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite diff --git a/src/jrd/tests/json/JsonPathParsingTest.cpp b/src/jrd/tests/json/JsonPathParsingTest.cpp new file mode 100644 index 00000000000..09a703ee3e4 --- /dev/null +++ b/src/jrd/tests/json/JsonPathParsingTest.cpp @@ -0,0 +1,452 @@ +#include "boost/test/unit_test.hpp" +#include "JsonTestUtils.h" + +#include "firebird.h" +#include "../common/status.h" + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(JsonPathTests) + +using namespace FBJSON; +using namespace Firebird; +using namespace TestUtils; + +std::optional hasUnwrapFlag(const char* path) +{ + PathWrapper parsedPath(path); + if (!parsedPath.jpath.hasData()) + return std::nullopt; + + PathNode* pathNode = parsedPath.jpath->getJsonPath()->getRootNode(); + BOOST_TEST(pathNode != nullptr); + + if (pathNode == nullptr) + return false; + + while (pathNode->next) + { + pathNode = pathNode->next; + } + + BOOST_TEST_INFO(path); + return pathNode->matchUnwrapPattern(); +} + + +BOOST_AUTO_TEST_SUITE(PathParsingTests) + + +BOOST_AUTO_TEST_CASE(UnwrapFlagTest) +{ + BOOST_CHECK(hasUnwrapFlag("+$.digits") == true); + BOOST_CHECK(hasUnwrapFlag("-$.digits") == true); + BOOST_CHECK(hasUnwrapFlag("$.digits + 22") == false); + BOOST_CHECK(hasUnwrapFlag("22 + $.digits") == false); + BOOST_CHECK(hasUnwrapFlag("($.digits + 22) ? (@ > 0)") == false); + BOOST_CHECK(hasUnwrapFlag("22 + $.digits ? (@ > 0)") == true); + BOOST_CHECK(hasUnwrapFlag(R"(lax $ ? (@.type() == "array"))") == true); + + BOOST_CHECK(hasUnwrapFlag("strict +$.digits") == false); + BOOST_CHECK(hasUnwrapFlag("strict -$.digits") == false); + BOOST_CHECK(hasUnwrapFlag("strict $.digits + 22") == false); + BOOST_CHECK(hasUnwrapFlag("strict 22 + $.digits") == false); + BOOST_CHECK(hasUnwrapFlag("strict ($.digits + 22) ? (@ > 0)") == false); + BOOST_CHECK(hasUnwrapFlag("strict (22 + $.digits) ? (@ > 0)") == false); + BOOST_CHECK(hasUnwrapFlag("strict 22 + $.digits ? (@ > 0)") == false); + + BOOST_CHECK(hasUnwrapFlag("+$.digits[*]") == true); + BOOST_CHECK(hasUnwrapFlag("-$.digits[*]") == true); + BOOST_CHECK(hasUnwrapFlag("$.digits[*] + 22") == false); + BOOST_CHECK(hasUnwrapFlag("22 + $.digits[*]") == false); + BOOST_CHECK(hasUnwrapFlag("($.digits[*] + 22) ? (@ > 0)") == false); + BOOST_CHECK(hasUnwrapFlag("22 + $.digits[*] ? (@ > 0)") == true); + BOOST_CHECK(hasUnwrapFlag("(22 + $.digits[*]) ? (@ > 0)") == false); +} + + +BOOST_AUTO_TEST_CASE(KeyValueMethodTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue()"); + + // The root node is the method + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(expr->getChildrenCount() == FB_SIZE_T(1)); + + // The child is a root + BOOST_REQUIRE(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); +} + +BOOST_AUTO_TEST_CASE(KeyValue_PathTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue().value"); + + // The keyvalue method + BOOST_CHECK(expr->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(expr->getChildrenCount() == FB_SIZE_T(2)); + + // The path root + BOOST_CHECK(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); + + // The path after the method + BOOST_CHECK(expr->getLastChild()->isVariable()); + var = expr->getLastChild()->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(var->path->getRootNode()->next); + BOOST_CHECK(var->path->getRootNode()->next->next == nullptr); + BOOST_CHECK(var->path->getRootNode()->next->field.equals("value")); +} + +BOOST_AUTO_TEST_CASE(KeyValue_KeyValueTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue().keyvalue()"); + BOOST_CHECK(expr->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); + BOOST_REQUIRE(expr->getChildrenCount() == 2); + BOOST_REQUIRE(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); + + // Is keyvalue node + JsonExprNode* keyvalue = expr->getLastChild(); + BOOST_CHECK(keyvalue->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(keyvalue->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(keyvalue->getChildrenCount() == ULONG(1)); + + // Only a placement argument + BOOST_REQUIRE(keyvalue->getFirstChild()->isVariable()); + BOOST_CHECK(keyvalue->getFirstChild()->getVariable()->type == PathVariable::Type::HEAD); +} + +BOOST_AUTO_TEST_CASE(KeyValue_Path_KeyValueTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue().value.keyvalue()"); + // MN(keyvalue()) + // | | + // VN($) VN(&.value) + // | + // MN(keyvalue()) + // | + // VN(&) + + // I. The root path part - "keyvalue()" + BOOST_CHECK(expr->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); // keyvalue() + BOOST_REQUIRE(expr->getChildrenCount() == 2); + + // I. Arg 1. The root path - "$" + BOOST_REQUIRE(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); + + // I. Arg 2. The afterpath variable - ".value" + JsonExprNode* afterpath = expr->getLastChild(); + BOOST_CHECK(afterpath->testType(JsonExprNode::VARIABLE_NODE)); + var = afterpath->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(var->path->getRootNode()->next); + BOOST_CHECK(var->path->getRootNode()->next->field.equals("value")); + BOOST_CHECK(var->path->getRootNode()->next->matchUnwrapPattern()); + BOOST_CHECK(var->path->getRootNode()->next->next == nullptr); + + // II. The second method - keyvalue() + BOOST_REQUIRE(afterpath->getChildrenCount() == ULONG(1)); + JsonExprNode* keyvalue = afterpath->getLastChild(); + BOOST_CHECK(keyvalue->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(keyvalue->getPathMethod() == PathMethod::KEYVALUE); + + // II. Arg 1. The chain variable - & (empty path) + BOOST_TEST(keyvalue->getChildrenCount() == ULONG(1)); + JsonExprNode* chainArg = keyvalue->getLastChild(); + BOOST_CHECK(chainArg->testType(JsonExprNode::VARIABLE_NODE)); + BOOST_CHECK(chainArg->getVariable()); + var = chainArg->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_CHECK(var->path->isEmpty(0)); +} + + +BOOST_AUTO_TEST_CASE(KeyValueMethodWithPathTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue().keyvalue().value"); + + // The first keyvalue + BOOST_CHECK(expr->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(expr->getChildrenCount() == 2U); + + // The root + BOOST_REQUIRE(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); + + // The second keyvalue + JsonExprNode* keyvalue = expr->getLastChild(); + BOOST_CHECK(keyvalue->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(keyvalue->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(keyvalue->getChildrenCount() == 2U); + + // The dummy argument + BOOST_REQUIRE(keyvalue->getFirstChild()->isVariable()); + var = keyvalue->getFirstChild()->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + + // Keyvalue unwraps the argument + BOOST_REQUIRE(var->path != nullptr); + BOOST_REQUIRE(var->path->getRootNode() != nullptr); + BOOST_CHECK(var->path->getRootNode()->matchUnwrapPattern()); + + // The path after the method + BOOST_REQUIRE(keyvalue->getLastChild()->isVariable()); + var = keyvalue->getLastChild()->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(var->path->getRootNode()->next); + BOOST_CHECK(var->path->getRootNode()->next->next == nullptr); + BOOST_CHECK(var->path->getRootNode()->next->field.equals("value")); +} + + +BOOST_AUTO_TEST_CASE(MultipleKeyValuesTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue().value.a.keyvalue().value.b.keyvalue().name"); + + // I. The first keyvalue + BOOST_CHECK(expr->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(expr->getChildrenCount() == ULONG(2)); + + // The root + BOOST_REQUIRE(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); + BOOST_CHECK(expr->getFirstChild()->getVariable()->path == nullptr); + + // The path "value.a" + JsonExprNode* afterpath = expr->getLastChild(); + BOOST_REQUIRE(afterpath->isVariable()); + var = afterpath->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(var->path); + BOOST_REQUIRE(var->path->getRootNode()); + BOOST_CHECK(var->path->getRootNode()->next); + BOOST_REQUIRE(var->path->getRootNode()->next->next); + BOOST_CHECK(var->path->getRootNode()->next->next->next == nullptr); + BOOST_CHECK(var->path->getRootNode()->next->field.equals("value")); + BOOST_CHECK(var->path->getRootNode()->next->next->field.equals("a")); + + // II. The second keyvalue + JsonExprNode* keyvalue = afterpath->getLastChild(); + BOOST_CHECK(keyvalue->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(keyvalue->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(keyvalue->getChildrenCount() == ULONG(2)); + + // The second keyvalue chain arg + JsonExprNode* chainArg = keyvalue->getFirstChild(); + BOOST_REQUIRE(chainArg->isVariable()); + var = chainArg->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(!var->hasPath(0)); + + // The second keyvalue afterpath + afterpath = keyvalue->getLastChild(); + BOOST_REQUIRE(afterpath->isVariable()); + var = afterpath->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(var->path); + BOOST_REQUIRE(var->path->getRootNode()); + BOOST_CHECK(var->path->getRootNode()->next); + BOOST_REQUIRE(var->path->getRootNode()->next->next); + BOOST_CHECK(var->path->getRootNode()->next->next->next == nullptr); + BOOST_CHECK(var->path->getRootNode()->next->field.equals("value")); + BOOST_CHECK(var->path->getRootNode()->next->next->field.equals("b")); + + + // III. The third keyvalue + keyvalue = afterpath->getLastChild(); + BOOST_CHECK(keyvalue->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(keyvalue->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(keyvalue->getChildrenCount() == ULONG(2)); + + // Chin arg + afterpath = keyvalue->getFirstChild(); + BOOST_REQUIRE(afterpath->isVariable()); + var = afterpath->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_REQUIRE(!var->hasPath(0)); + + // The path "value" after the thread keyvalue + BOOST_REQUIRE(keyvalue->getLastChild()->isVariable()); + var = keyvalue->getLastChild()->getVariable(); + BOOST_CHECK(var->type == PathVariable::Type::HEAD); + BOOST_CHECK(var->path->getRootNode()->next); + BOOST_CHECK(var->path->getRootNode()->next->next == nullptr); + BOOST_CHECK(var->path->getRootNode()->next->field.equals("name")); +} + + + +BOOST_AUTO_TEST_CASE(KeyValueMethodWithFilterTest) +{ + const JsonExprNode* expr = nullptr; + const PathVariable* var = nullptr; + + PathWrapper holder; + expr = holder.getPathTailNodes("$.keyvalue() ? (@ > 3)"); + + // The first keyvalue + BOOST_CHECK(expr->testType(JsonExprNode::METHOD_NODE)); + BOOST_CHECK(expr->getPathMethod() == PathMethod::KEYVALUE); + BOOST_TEST(expr->getChildrenCount() == 2U); + + // The root + BOOST_REQUIRE(expr->getFirstChild()->isVariable()); + BOOST_CHECK(expr->getFirstChild()->getVariable()->type == PathVariable::Type::ROOT); + + // The filter + JsonExprNode* afterpath = expr->getLastChild(); + BOOST_REQUIRE(afterpath->testType(JsonExprNode::VARIABLE_NODE)); + + auto* path = afterpath->getVariable()->path->getRootNode(); + BOOST_REQUIRE(path->filterNode != nullptr); +} + +BOOST_AUTO_TEST_CASE(FilterForZeroPathTest) +{ + const PathVariable* var = nullptr; + + PathWrapper pathWrapper("$ ? (@ == 42)"); + auto* path = pathWrapper.jpath->getJsonPath()->getRootNode(); + BOOST_REQUIRE(path); + BOOST_REQUIRE(path); + BOOST_TEST(path->next == nullptr); + BOOST_TEST(path->filterNode != nullptr); +} + +BOOST_AUTO_TEST_CASE(SyntaxTest) +{ + PathWrapper parser; + parser.keys.put("pass"); + parser.keys.put("hello"); + parser.keys.put("passing"); + { + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$")); + BOOST_CHECK_THROW(parser.parseNoCheck("@"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$passing")); // Allow + BOOST_CHECK_THROW(parser.parseNoCheck("4"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("null"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("false"), Firebird::Exception); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("lax $")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("strict $")); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.field")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.\"field\"")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.*")); + BOOST_CHECK_THROW(parser.parseNoCheck("$.123"), json_syntax_exception); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[1,2,3]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[1]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[*]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[1 to 2]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[2 to 1]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[-1]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[-1 to -4]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[-1 to last]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[-1 to $pass]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[$pass to 2]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[$pass to $hello]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[$.field to (1 + 2)]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[($.field ? (@ > 3)) to (1 + 2)]")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$[($.field.abs()) to (1 + 2)]")); + BOOST_CHECK_THROW(parser.parseNoCheck("$[$unknown to 2]"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$[sdd]"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$[1 to dd]"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$[1 to to]"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$[to 2]"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$[2 to]"), Firebird::Exception); + + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@.value > 3)")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@.value > 3).hello")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@.value > 3).hello ? (@.r == 2)")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@.value > 3).hello ? (@.r == 2)[1,2,3]")); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.abs()")); + BOOST_CHECK_THROW(parser.parseNoCheck("$.kfds()"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.abs().value"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.keyvalue()")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.keyvalue().value")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.keyvalue().value")); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@.value.abs() > 3)")); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? (@.value.abs().value > 3)"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@.value.keyvalue().value > 3)")); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? ((@ > 3)) && ($ > 3))"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? ((@ > 3) && ($ > 3))")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@ > 3 && $ > 3)")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (($passing > 3) && (4 > 3))")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (($.value.abs() > 3) && (4 > 3))")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? ($passing > 3 && 4 > 3)")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? ($.value.abs() > 3 && 4 > 3)")); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? (($passing > 3)) && (4 > 3))"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? (($.value.abs() > 3)) && (4 > 3))"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? ((4 && 3))"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? (4 && 3)"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? (4 && 3)"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name ? (false && false)"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name ? (@ > 3) ? (@ > 3)")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name.abs().abs()")); + BOOST_CHECK_THROW(parser.parseNoCheck("$.name.abs().value"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name.keyvalue().value")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.keyvalue() ? (@ > 3).v ? (@ > 3)")); + + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("3 + $.value")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.value + 3")); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$passing + 3")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("3 + $passing")); + + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$passing + $.value")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.value + $passing")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.value + 4")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("4 + $.value")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$passing ? ($.value > 2)")); // Allow passing as main expr + BOOST_CHECK_THROW(parser.parseNoCheck("$passing ? ($.value)"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$.value ? ($.value)"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("$@ ? ($.value)"), Firebird::Exception); + BOOST_CHECK_THROW(parser.parseNoCheck("@ ? ($.value)"), Firebird::Exception); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name + 3")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name + $.value")); + BOOST_CHECK_NO_THROW(parser.parseNoCheck("$.name + $.abs() + 4")); + + } +} + + +BOOST_AUTO_TEST_SUITE_END() // PathParsingTests + +BOOST_AUTO_TEST_SUITE_END() // JsonPathTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite diff --git a/src/jrd/tests/json/JsonTestUtils.h b/src/jrd/tests/json/JsonTestUtils.h new file mode 100644 index 00000000000..614ccb49803 --- /dev/null +++ b/src/jrd/tests/json/JsonTestUtils.h @@ -0,0 +1,116 @@ +#ifndef JSON_TEST_UTIL_H +#define JSON_TEST_UTIL_H + +#include "firebird.h" +#include "../common/status.h" +#include "../jrd/json/path/JsonPath.h" +#include "../jrd/json/path/JPathParser.h" +#include "../jrd/json/JsonRuntime.h" +#include "../jrd/json/JsonUtils.h" + +#include "boost/test/unit_test.hpp" + +#ifdef DEV_BUILD +#define SKIP_IN_CI +#else +#define SKIP_IN_CI return; +#endif + +#define SKIP_TEST return; + +namespace TestUtils { + +template +inline void reportError(const TStatus& status, const Firebird::string& debug) +{ + if (status->isDirty()) + { + BOOST_TEST_INFO(debug.data()); + + const ISC_STATUS* vector = status->getErrors(); + SCHAR s[BUFFER_LARGE]; + + Firebird::string info; + while (fb_interpret(s, sizeof(s), &vector)) + { + info += s; + info += "\n"; + } + + BOOST_TEST_INFO(info.data()); + } +} + +inline FBJSON::JsonPathExpr* parsePath(const std::string_view path, const PassingKeys* keys, bool rethrow = false) +{ + try + { + FBJSON::PathParser parser(*getDefaultMemoryPool(), path); + return parser.parse(keys); + } + catch (const Firebird::Exception& ex) + { + Firebird::FbLocalStatus status; + ex.stuffException(&status); + + Firebird::string debug; + debug.printf("The problem path is: %s", path.data()); + reportError(status, debug); + + if (rethrow) + throw; + + return {}; + } +} + +class PathWrapper +{ +public: + Firebird::AutoPtr jpath = nullptr; + PassingKeys keys; + + PathWrapper() + { } + + PathWrapper(const std::string_view strPath) + { + parse(strPath); + } + + PathWrapper(PathWrapper&& other) + { + jpath = other.jpath.release(); + } + + PathWrapper& operator=(PathWrapper&& other) + { + jpath = other.jpath.release(); + return *this; + } + + + void parse(const std::string_view strPath) + { + jpath = parsePath(strPath, &keys); + } + + void parseNoCheck(const std::string_view strPath) + { + jpath = parsePath(strPath, &keys, true); + } + + inline const FBJSON::JsonExprNode* getPathTailNodes(const std::string_view strPath) + { + jpath = parsePath(strPath, &keys); + BOOST_REQUIRE(jpath != nullptr); + BOOST_REQUIRE(jpath->getTail() != nullptr); + + return jpath->getTail(); + } + +}; + +} + +#endif // !JSON_TEST_UTIL_H diff --git a/src/jrd/tests/json/classes/JsonDatetimeTest.cpp b/src/jrd/tests/json/classes/JsonDatetimeTest.cpp new file mode 100644 index 00000000000..15adc4fd1d5 --- /dev/null +++ b/src/jrd/tests/json/classes/JsonDatetimeTest.cpp @@ -0,0 +1,374 @@ +#include "boost/test/unit_test.hpp" +#include "../../TestContext.h" + +#include "../jrd/json/classes/JsonDatetime.h" + +#include "../common/tests/CvtTestUtils.h" +#include "../jrd/cvt_proto.h" +#include "fb_exception.h" + +#include +#include + +using namespace FBJSON; + + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(JsonTypesTests) + +BOOST_AUTO_TEST_SUITE(JsonDatetimeTests) + +constexpr int YEAR = 2023; + +bool compareDatetime(ISC_TIMESTAMP_TZ result, ISC_TIMESTAMP_TZ expected) +{ + struct tm resultTimes; + memset(&resultTimes, 0, sizeof(resultTimes)); + int resultFractions; + NoThrowTimeStamp::decode_timestamp(result.utc_timestamp, &resultTimes, &resultFractions); + SSHORT resultOffset; + TimeZoneUtil::extractOffset(result, &resultOffset); + + struct tm expectedTimes; + memset(&expectedTimes, 0, sizeof(expectedTimes)); + int expectedFractions; + NoThrowTimeStamp::decode_timestamp(expected.utc_timestamp, &expectedTimes, &expectedFractions); + SSHORT expectedOffset; + TimeZoneUtil::extractOffset(expected, &expectedOffset); + + return !((bool) memcmp(&resultTimes, &expectedTimes, sizeof(struct tm))) + && resultFractions == expectedFractions && resultOffset == expectedOffset; +} + +bool compareTimeOnly(ISC_TIMESTAMP_TZ result, ISC_TIMESTAMP_TZ expected) +{ + struct tm resultTimes; + memset(&resultTimes, 0, sizeof(resultTimes)); + NoThrowTimeStamp::decode_timestamp(result.utc_timestamp, &resultTimes); + + struct tm expectedTimes; + memset(&expectedTimes, 0, sizeof(expectedTimes)); + NoThrowTimeStamp::decode_timestamp(expected.utc_timestamp, &expectedTimes); + + return resultTimes.tm_hour == expectedTimes.tm_hour && + resultTimes.tm_min == expectedTimes.tm_min && + resultTimes.tm_sec == expectedTimes.tm_sec; +} + +static void errFunc(const Firebird::Arg::StatusVector& v) +{ + v.raise(); +} + +BOOST_FIXTURE_TEST_CASE(SetFromStringTests, EngineHolder) +{ + using namespace std::string_view_literals; + + JsonDatetime test(*getDefaultMemoryPool()); + BOOST_CHECK_THROW(test.set("1443324 5123332"sv, ""), Firebird::Exception); + + CvtTestUtils::MockCallback cb(errFunc, std::bind(CvtTestUtils::mockGetLocalDate, YEAR)); + + test.set("2018-01-01"sv, "", &cb); + BOOST_TEST(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(2018, 1, 1, 0, 0, 0, 0))); + BOOST_TEST(test.getTypeName() == Tokens::TypeName::DATE); + + + test.set("01:23:45 +02:00"sv, "", &cb); + BOOST_TEST(compareTimeOnly(test.getTS(), + CvtTestUtils::createTimeStampTZ(0, 0, 0, 1, 23, 45, 120))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIME_WITH_TZ); + + + test.set("01:23:45"sv, "", &cb); + BOOST_TEST(compareTimeOnly(test.getTS(), + CvtTestUtils::createTimeStampTZ(0, 0, 0, 1, 23, 45, 0))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIME_WITHOUT_TZ); + + + test.set("2018-01-01 01:23:45 +02:00"sv, "", &cb); + BOOST_TEST(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(2018, 1, 1, 1, 23, 45, 120))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); + + + test.set("2018-01-01 01:23:45"sv, "", &cb); + BOOST_TEST(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(2018, 1, 1, 1, 23, 45, 0))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITHOUT_TZ); +} + + +BOOST_FIXTURE_TEST_CASE(SetFromStringWithFormatTests, EngineHolder) +{ + using namespace std::string_view_literals ; + + CvtTestUtils::MockCallback cb(errFunc, std::bind(CvtTestUtils::mockGetLocalDate, YEAR)); + JsonDatetime test(*getDefaultMemoryPool()); + + test.set("14"sv, "DD"sv, &cb); + BOOST_CHECK(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(0, 0, 14, 0, 0, 0, 0))); + BOOST_TEST(test.getTypeName() == Tokens::TypeName::DATE); + + + test.set("01:23 +02"sv, "HH24:MI TZH"sv, &cb); + BOOST_CHECK(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(0, 0, 0, 1, 23, 0, 120))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIME_WITH_TZ); + + + test.set("01:23:00"sv, "HH24:MI:SS"sv, &cb); + BOOST_CHECK(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(0, 0, 0, 1, 23, 0, 0))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIME_WITHOUT_TZ); + + + test.set("2018 04 02 01:23 +02"sv, "YYYY DD MM HH24:MI TZH"sv, &cb); + BOOST_CHECK(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(2018, 2, 4, 1, 23, 0, 120))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); + + + test.set("2018 04 02 01:23"sv, "YYYY DD MM HH24:MI"sv, &cb); + BOOST_CHECK(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(2018, 2, 4, 1, 23, 0, 0))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITHOUT_TZ); + + + test.set("14 5"sv, "DD TZH"sv, &cb); + BOOST_CHECK(compareDatetime(test.getTS(), + CvtTestUtils::createTimeStampTZ(00, 0, 14, 0, 0, 0, 300))); + + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); +} + + +BOOST_FIXTURE_TEST_CASE(RedefineDatetimeFormatTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime datetime(*getDefaultMemoryPool()); + CvtTestUtils::MockCallback cb(errFunc, std::bind(CvtTestUtils::mockGetLocalDate, YEAR)); + + // Time + datetime.set("12:10"sv, "", &cb); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIME_WITHOUT_TZ); + BOOST_CHECK(compareTimeOnly(datetime.getTS(), + CvtTestUtils::createTimeStampTZ(0, 0, 0, 12, 10, 0, 0))); + + // Date + datetime.updateFormat("MM:DD"sv, &cb); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::DATE); + BOOST_CHECK(compareDatetime(datetime.getTS(), + CvtTestUtils::createTimeStampTZ(0, 12, 10, 0, 0, 0, 0))); +} + +BOOST_FIXTURE_TEST_CASE(InvalidInputTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime datetime(*getDefaultMemoryPool()); + BOOST_CHECK_THROW(datetime.set("14 5"sv, "DD"), Firebird::Exception); + BOOST_CHECK_THROW(datetime.set("14 532fe4"sv, ""), Firebird::Exception); +} + +void testMore(JsonDatetime dateA, JsonDatetime dateB) +{ + // A vs B + BOOST_CHECK(dateA > dateB); + BOOST_CHECK((dateA < dateB) == false); + BOOST_CHECK((dateA == dateB) == false); + BOOST_CHECK((dateA != dateB) == true); + + // A vs A + BOOST_CHECK(dateA == dateA); + BOOST_CHECK((dateA != dateA) == false); +} + +BOOST_FIXTURE_TEST_CASE(DatetimeCompareTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime dateA(*getDefaultMemoryPool()); + JsonDatetime dateB(*getDefaultMemoryPool()); + + // Date + dateA.set("2018-02-01"sv); + + dateB.set("2018-01-01"sv); + + testMore(dateA, dateB); + + // Timestamp + dateA.set("2018-01-01 01:25:45"sv); + + dateB.set("2018-01-01 01:23:45"sv); + testMore(dateA, dateB); + + // Time + dateA.set("01:25:45"sv); + dateB.set("01:23:45"sv); + testMore(dateA, dateB); +} + +BOOST_FIXTURE_TEST_CASE(DatetimeMethodsTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime date(*getDefaultMemoryPool()); + + // We need a bit string to test the move + constexpr std::string_view sourceStr = "12:10 "sv; + SmallString toString(*getDefaultMemoryPool()); + + static_assert(sourceStr.length() > Firebird::string::INLINE_BUFFER_SIZE); + SmallString fromString(*getDefaultMemoryPool(), sourceStr.data(), sourceStr.length()); + date.set(std::move(fromString), ""sv); + + // Should be empty after the move + BOOST_TEST(fromString.empty()); + + // Move from datetime + toString = date.extractString(); + BOOST_TEST((std::string_view)toString == sourceStr); + + // Should be empty after the move + toString = date.extractString(); + BOOST_TEST(toString.empty()); +} + +BOOST_FIXTURE_TEST_CASE(ZeroTimeTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime datetime(*getDefaultMemoryPool()); + + // Time + datetime.set("00:00"sv, ""); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIME_WITHOUT_TZ); + + datetime.set("00:00 +2:00"sv, ""); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIME_WITH_TZ); + + datetime.set("13.01.2023 00:00"sv, ""); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIMESTAMT_WITHOUT_TZ); + + datetime.set("13.01.2023 00:00 +2:00"sv, ""); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); +} + +BOOST_FIXTURE_TEST_CASE(UtcTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime timeBase(*getDefaultMemoryPool()); + JsonDatetime timeTs(*getDefaultMemoryPool()); + JsonDatetime timeTsSame(*getDefaultMemoryPool()); + + // Timestamp + timeBase.set("10.10.2022 10:3:10 +00:00"sv, ""sv); + timeTs.set("10.10.2022 10:3:10 -00:02"sv, ""sv); + timeTsSame.set("10.10.2022 10:1:10 -00:02"sv, ""sv); + testMore(timeTs, timeBase); + + BOOST_CHECK(timeBase == timeTsSame); + + // Time + timeBase.set("10:3:10 +00:00"sv, ""sv); + timeTs.set("10:3:10 -00:02"sv, ""sv); + timeTsSame.set("10:1:10 -00:02"sv, ""sv); + testMore(timeTs, timeBase); + + BOOST_CHECK(timeBase == timeTsSame); +} + +BOOST_FIXTURE_TEST_CASE(SetFromDscTests, EngineHolder) +{ + using namespace std::string_view_literals; + JsonDatetime textDt(*getDefaultMemoryPool()); + JsonDatetime dscTd(*getDefaultMemoryPool()); + + dsc textDsc; + UCHAR buffer[64]; + dsc outDsc; + + { + auto sourceTime = "10:03"sv; + textDt.set(sourceTime, ""sv); + + textDsc.makeText(sourceTime.length(), ttype_ascii, (UCHAR*)sourceTime.data()); + + outDsc.makeTime(); + outDsc.dsc_address = buffer; + CVT_move(&textDsc, &outDsc, 0); + dscTd.set(JRD_get_thread_data(), outDsc); + + BOOST_CHECK(textDt == dscTd); + } + + { + auto sourceTime = "10:03 +00:00"sv; + textDt.set(sourceTime, ""sv); + + textDsc.makeText(sourceTime.length(), ttype_ascii, (UCHAR*)sourceTime.data()); + + outDsc.makeTimeTz(); + outDsc.dsc_address = buffer; + CVT_move(&textDsc, &outDsc, 0); + dscTd.set(JRD_get_thread_data(), outDsc); + + BOOST_CHECK(textDt == dscTd); + } + + { + auto sourceTime = "10.03.2011"sv; + textDt.set(sourceTime, ""sv); + + textDsc.makeText(sourceTime.length(), ttype_ascii, (UCHAR*)sourceTime.data()); + + outDsc.makeDate(); + outDsc.dsc_address = buffer; + CVT_move(&textDsc, &outDsc, 0); + dscTd.set(JRD_get_thread_data(), outDsc); + + BOOST_CHECK(textDt == dscTd); + } + + { + auto sourceTime = "10.03.2011 10:20"sv; + textDt.set(sourceTime, ""sv); + + textDsc.makeText(sourceTime.length(), ttype_ascii, (UCHAR*)sourceTime.data()); + + outDsc.makeTimestamp(); + outDsc.dsc_address = buffer; + CVT_move(&textDsc, &outDsc, 0); + dscTd.set(JRD_get_thread_data(), outDsc); + + BOOST_CHECK(textDt == dscTd); + } + + { + auto sourceTime = "10.03.2011 10:20 +00:00"sv; + textDt.set(sourceTime, ""sv); + + textDsc.makeText(sourceTime.length(), ttype_ascii, (UCHAR*)sourceTime.data()); + + outDsc.makeTimestampTz(); + outDsc.dsc_address = buffer; + CVT_move(&textDsc, &outDsc, 0); + dscTd.set(JRD_get_thread_data(), outDsc); + + BOOST_CHECK(textDt == dscTd); + } +} + + +BOOST_AUTO_TEST_SUITE_END() // JsonDatetimeTests + +BOOST_AUTO_TEST_SUITE_END() // JsonTypesTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite diff --git a/src/jrd/tests/json/classes/JsonPathTest.cpp b/src/jrd/tests/json/classes/JsonPathTest.cpp new file mode 100644 index 00000000000..bf5400b36a0 --- /dev/null +++ b/src/jrd/tests/json/classes/JsonPathTest.cpp @@ -0,0 +1,1112 @@ +#include "boost/test/unit_test.hpp" +#include "../../TestContext.h" + +#include "../jrd/json/path/JsonPath.h" +#include "../jrd/json/classes/JsonScalar.h" +#include "../jrd/json/JsonRuntime.h" + +#include + +using namespace FBJSON; + + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(JsonClassesTests) + +BOOST_AUTO_TEST_SUITE(PathNodeTests) + + +BOOST_AUTO_TEST_CASE(RangeTest) +{ + { + JsonPath::Range range; + range.init(42); + BOOST_TEST(range.down == 42); + BOOST_TEST(range.up == 42); + + range.init(-42, -3); + BOOST_TEST(range.down == -42); + BOOST_TEST(range.up == -3); + } + + { + JsonPath::Range range; + range.init(-8, -3); + BOOST_TEST(range.getPreparedDown(10) == 2); + BOOST_TEST(range.getPreparedUp(10) == 7); + range.init(5, 42); + BOOST_TEST(range.getPreparedDown(10) == 5); + BOOST_TEST(range.getPreparedUp(120) == 42); + } + + { + JsonPath::Range range; + range.init(-8, -3); + BOOST_TEST(range.getPreparedDown(10) == 2); + BOOST_TEST(range.getPreparedUp(10) == 7); + range.init(5, 42); + BOOST_TEST(range.getPreparedDown(10) == 5); + BOOST_TEST(range.getPreparedUp(120) == 42); + } + + { + JsonPath::Range range; + range.init(5, 15); + BOOST_TEST(range.enters(4) == false); + BOOST_TEST(range.enters(7) == true); + BOOST_TEST(range.enters(2) == false); + BOOST_TEST(range.enters(1) == false); + BOOST_TEST(range.enters(16) == false); + } + + { + JsonPath::Range range; + range.init(5, 15); + BOOST_TEST(range.canWrap() == false); + range.init(0, -2); + BOOST_TEST(range.canWrap() == true); + range.init(1, -1); + BOOST_TEST(range.canWrap() == true); + } + + { + JsonPath::Range range; + range.init(5, 15); + BOOST_TEST(range.canGetFirst(5) == false); + range.init(0, -2); + BOOST_TEST(range.canGetFirst(5) == true); + range.init(1, -1); + BOOST_TEST(range.canGetFirst(5) == false); + BOOST_TEST(range.canGetFirst(1) == true); + } + + { + JsonPath::Range range; + range.init(0, -1); + BOOST_TEST(range.isComplex() == false); + range.init(0, -2); + BOOST_TEST(range.isComplex() == true); + range.init(3, 4); + BOOST_TEST(range.isComplex() == false); + range.init(3, -1); + BOOST_TEST(range.isComplex() == true); + } + + { + JsonPath::Range range; + range.init(42); + BOOST_TEST(range.isWildcard() == false); + range.init(0, JPATH_ARRAY_LAST_INDEX); + BOOST_TEST(range.isWildcard() == true); + } +} + +JsonExprNode* makeDummyNode() +{ + return JsonExprNode::make(*getDefaultMemoryPool()); +} + +BOOST_AUTO_TEST_CASE(NodeMethodsTest) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + + { + PathNode node(pool); + BOOST_CHECK(node.state == PathNodeState::NORMAL); + BOOST_CHECK(!node.isTemporary()); + BOOST_CHECK(!node.isDisabled()); + BOOST_CHECK(node.isEmpty(0)); + } + + { // add + PathNode node(pool); + PathNode* next = node.add(); + BOOST_CHECK(node.next == next); + BOOST_CHECK(next->prev == &node); + + BOOST_CHECK(next->depth == node.depth + 1); + } + + { // enable disable first + PathNode node(pool); + auto next = node.add(); + node.enable(); // nothing + BOOST_CHECK(node.state == PathNodeState::NORMAL); + BOOST_CHECK(!node.isTemporary()); + BOOST_CHECK(!node.isDisabled()); + BOOST_CHECK(!node.isEmpty(0)); + BOOST_TEST(next->depth == node.depth + 1); + + node.disable(); + BOOST_CHECK(node.state == PathNodeState::DISABLED); + BOOST_CHECK(!node.isTemporary()); + BOOST_CHECK(node.isDisabled()); + BOOST_CHECK(!node.isEmpty(0)); + BOOST_TEST(next->depth == node.depth); + + node.enable(); + BOOST_CHECK(node.state == PathNodeState::NORMAL); + BOOST_CHECK(!node.isTemporary()); + BOOST_CHECK(!node.isDisabled()); + BOOST_CHECK(!node.isEmpty(0)); + BOOST_TEST(next->depth == node.depth + 1); + } + + { // enable disable middle + PathNode first(pool); + PathNode* middle = first.add(); + PathNode* last = middle->add(); + + middle->disable(); + BOOST_CHECK(&first == last->prev); + BOOST_CHECK(first.next == middle); + BOOST_CHECK(first.depth + 1 == last->depth); + + BOOST_CHECK(middle->next == last); // Does not changes + BOOST_CHECK(middle->depth == last->depth); + middle->enable(); + BOOST_CHECK(middle == last->prev); + BOOST_CHECK(first.next == middle); + BOOST_CHECK(first.depth + 1 == middle->depth); + BOOST_CHECK(middle->next == last); + BOOST_CHECK(middle->depth + 1 == last->depth); + } + + { // enable disable last + PathNode first(pool); + PathNode* middle = first.add(); + PathNode* last = middle->add(); + + last->disable(); + BOOST_CHECK(last->isDisabled()); + BOOST_CHECK(last->next == nullptr); + last->enable(); + BOOST_CHECK(last->state == FBJSON::PathNodeState::NORMAL); + } + + { // selectAll + PathNode node(pool); + node.selectAll(); + BOOST_CHECK(node.isNextEmpty()); + BOOST_CHECK(node.type == ItemType::ARRAY_ELEMENT); + BOOST_REQUIRE(node.ranges.hasData()); + BOOST_TEST(node.ranges[0].down == 0); + BOOST_TEST(node.ranges[0].up == JPATH_ARRAY_LAST_INDEX); + } + + { // Updates disabled + PathNode node(pool); + PathNode* middle = node.add(); + PathNode* last = middle->add(); + + middle->disable(); + auto updated = middle->update(); + BOOST_CHECK(updated == middle); + + BOOST_CHECK(node.next == middle); + BOOST_CHECK(middle->next == last); + + BOOST_TEST(node.depth + 1 == middle->depth); + BOOST_TEST(middle->depth + 1 == last->depth); + } + + { // Updates temporally middle + PathNode node(pool); + PathNode* middle = node.add(); + middle->state = PathNodeState::TEMPORARY; + PathNode* last = middle->add(); + + auto updated = middle->update(); // deletes middle + BOOST_CHECK(updated == last); + + BOOST_CHECK(node.next == last); + BOOST_CHECK(last->prev == &node); + + BOOST_TEST(node.depth + 1 == last->depth); + } + + { // Updates temporally last + PathNode node(pool); + PathNode* middle = node.add(); + PathNode* last = middle->add(); + last->state = PathNodeState::TEMPORARY; + + auto updated = last->update(); + BOOST_CHECK(updated == middle); + BOOST_CHECK(middle->next == nullptr); + } + + { // updateLaxPatterns + PathNode node(pool); + node.ranges.add().init(0, 3); + node.ranges.add().init(-1, 3); + node.ranges.add().init(5, 10); + node.updateLaxPatterns(); + BOOST_CHECK(node.matchWrapPattern()); + node.ranges.clear(); + + node.ranges.add().init(-1, 3); + node.ranges.add().init(0, -3); + node.updateLaxPatterns(); + BOOST_CHECK(node.matchWrapPattern()); + node.ranges.clear(); + + node.ranges.add().init(-1, 3); + node.ranges.add().init(0, -3); + node.ranges.add().init(5, 10); + node.updateLaxPatterns(); + BOOST_CHECK(node.matchWrapPattern()); + node.ranges.clear(); + } + + { // insertUnwrapNode last + PathNode node(pool); + node.filterNode = makeDummyNode(); + PathNode* next = node.insertUnwrapNode(); + + BOOST_CHECK(next->state == FBJSON::PathNodeState::TEMPORARY); + BOOST_CHECK(next->filterNode.hasData()); + BOOST_CHECK(next->prev == &node); + BOOST_CHECK(next == node.next); + BOOST_CHECK(!next->matched); + BOOST_REQUIRE(next->ranges.hasData()); + BOOST_CHECK(next->ranges[0].canWrap()); + } + + { // insertUnwrapNode middle + PathNode node(pool); + PathNode* middle = node.add(); + PathNode* last = middle->add(); + + middle->filterNode = makeDummyNode(); + PathNode* temp = middle->insertUnwrapNode(); + + BOOST_CHECK(temp->state == FBJSON::PathNodeState::TEMPORARY); + BOOST_CHECK(temp->filterNode.hasData()); + BOOST_CHECK(temp->prev == middle); + BOOST_CHECK(middle->next == temp); + BOOST_CHECK(temp->next == last); + BOOST_CHECK(last->prev == temp); + } + + { // insert + PathNode node(pool); + PathNode* next = node.insert(); + + BOOST_CHECK(next->state == FBJSON::PathNodeState::NORMAL); + BOOST_CHECK(next->prev == &node); + BOOST_CHECK(next == node.next); + } + + { // insert + PathNode node(pool); + PathNode* middle = node.add(); + PathNode* last = middle->add(); + PathNode* temp = middle->insert(); + + BOOST_CHECK(temp->state == FBJSON::PathNodeState::NORMAL); + BOOST_CHECK(temp->prev == middle); + BOOST_CHECK(middle->next == temp); + BOOST_CHECK(temp->next == last); + BOOST_CHECK(last->prev == temp); + } + + + { // removeThis + Firebird::AutoPtr first = new PathNode(pool); + PathNode* middle = first->add(); + PathNode* last = middle->add(); + + first.release()->removeThis(); + + BOOST_CHECK(middle->prev == nullptr); + BOOST_TEST(middle->depth == 0); + BOOST_TEST(last->depth == 1); + } + + { // removeThis + Firebird::AutoPtr first = new PathNode(pool); + PathNode* middle = first->add(); + PathNode* last = middle->add(); + + middle->removeThis(); + + BOOST_CHECK(first->next == last); + BOOST_CHECK(last->prev == first); + BOOST_TEST(first->depth + 1 == last->depth); + } + + { // removeThis + Firebird::AutoPtr first = new PathNode(pool); + PathNode* middle = first->add(); + PathNode* last = middle->add(); + + last->removeThis(); + + BOOST_CHECK(middle->next == nullptr); + } + + + { // removeThis with temporally + Firebird::AutoPtr first = new PathNode(pool); + PathNode* last = first->add(); + auto* middle = first->insertUnwrapNode(); + middle->filterNode = makeDummyNode(); + + middle->removeThis(); + + BOOST_CHECK(first->filterNode.hasData()); + } + + + { // moveTo + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + middle->matched = true; + PathNode* last = middle->add(); + last->matched = true; + + BOOST_CHECK(last->moveTo(last->depth) == last); + BOOST_CHECK(last->matched == true); + BOOST_CHECK(last->moveTo(middle->depth) == middle); + BOOST_CHECK(last->matched == false); + BOOST_CHECK(middle->matched == false); + + last->matched = true; + middle->matched = true; + BOOST_CHECK(last->moveTo(first->depth) == first); + BOOST_CHECK(first->matched == false); + + first->state = FBJSON::PathNodeState::TEMPORARY; + last->matched = true; + middle->matched = true; + BOOST_CHECK(last->moveTo(first->depth) == first); + BOOST_CHECK(first->matched == false); + } + + { // moveTo + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + middle->matched = true; + PathNode* last = middle->add(); + last->disable(); + + BOOST_CHECK(last->moveTo(last->depth) == middle); + } + + + { // moveTo + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + middle->matched = true; + PathNode* last = middle->add(); + last->disable(); + + BOOST_CHECK(first->moveTo(middle->depth) == middle); + BOOST_CHECK(first->moveTo(middle->depth + 1) == nullptr); + + last->disable(); + BOOST_CHECK(first->moveTo(last->depth) == nullptr); + + last->enable(); + middle->disable(); + BOOST_CHECK(first->moveTo(middle->depth) == last); + } + + { // moveTo + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + middle->matched = true; + PathNode* last = middle->add(); + + BOOST_CHECK(first->moveTo(10) == nullptr); + } + + + { // moveTo + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + middle->matched = true; + PathNode* last = middle->add(); + + BOOST_CHECK(first->moveTo(middle->depth) == middle); + BOOST_CHECK(first->moveTo(last->depth) == last); + + last->disable(); + BOOST_CHECK(first->moveTo(last->depth) == nullptr); + + last->enable(); + middle->disable(); + BOOST_CHECK(first->moveTo(middle->depth) == last); + } + + + { // getRangeIndex + PathNode node(pool); + node.ranges.add().init(0,2); + + USHORT indexInRange = 0; + BOOST_CHECK(node.getRangeIndex(5, 0, indexInRange) == true); + BOOST_CHECK(indexInRange == 0); + + indexInRange++; + BOOST_CHECK(node.getRangeIndex(5, 0, indexInRange) == false); + } + + + { // getRangeIndex + PathNode node(pool); + node.ranges.add().init(0,2); + node.ranges.add().init(0,5); + node.ranges.add().init(3,5); + + USHORT indexInRange = 0; + BOOST_CHECK(node.getRangeIndex(5, 4, indexInRange) == true); + BOOST_TEST(indexInRange == 1); + + indexInRange++; + BOOST_CHECK(node.getRangeIndex(5, 4, indexInRange) == true); + BOOST_TEST(indexInRange == 2); + + indexInRange++; + BOOST_CHECK(node.getRangeIndex(5, 4, indexInRange) == false); + } + + + { // getRangeIndex + PathNode node(pool); + node.ranges.add().init(0,2); + node.ranges.add().init(0,5); + node.ranges.add().init(3,5); + + USHORT indexInRange = 0; + BOOST_CHECK(node.getRangeIndex(5, 4, indexInRange) == true); + BOOST_CHECK(indexInRange == 1); + + indexInRange++; + BOOST_CHECK(node.getRangeIndex(5, 4, indexInRange) == true); + BOOST_CHECK(indexInRange == 2); + + indexInRange++; + BOOST_CHECK(node.getRangeIndex(5, 4, indexInRange) == false); + } + + + { // matchFieldName + PathNode node(pool); + node.field = "hello"; + + BOOST_CHECK(node.matchFieldName("") == false); + BOOST_CHECK(node.matchFieldName("HELLO") == false); + BOOST_CHECK(node.matchFieldName("*") == false); + BOOST_CHECK(node.matchFieldName("hello") == true); + } + + { // matchFieldName + PathNode node(pool); + node.field = "*"; + + BOOST_CHECK(node.matchFieldName("") == false); + BOOST_CHECK(node.matchFieldName("HELLO") == true); + BOOST_CHECK(node.matchFieldName("*") == true); + BOOST_CHECK(node.matchFieldName("hello") == true); + } + + { // isInSimpleRange + PathNode node(pool); + BOOST_CHECK(node.isInSimpleRange(0) == false); + node.ranges.add().init(1,2); + node.ranges.add().init(1,5); + node.ranges.add().init(3,10); + + BOOST_CHECK(node.isInSimpleRange(1) == true); + BOOST_CHECK(node.isInSimpleRange(5) == true); + BOOST_CHECK(node.isInSimpleRange(0) == false); + BOOST_CHECK(node.isInSimpleRange(6) == true); + BOOST_CHECK(node.isInSimpleRange(10) == true); + BOOST_CHECK(node.isInSimpleRange(11) == false); + } + + { // validateRange + PathNode node(pool); + BOOST_CHECK_NO_THROW(node.validateRange(0)); + node.ranges.add().init(1,5); + node.ranges.add().init(-5,-1); + + BOOST_CHECK_THROW(node.validateRange(0), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(2), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(5), json_strict_exception); + BOOST_CHECK_NO_THROW(node.validateRange(6)); + + node.ranges.clear(); + node.ranges.add().init(-1,-2); // invalid range + BOOST_CHECK_THROW(node.validateRange(0), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(1), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(2), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(3), json_strict_exception); + + + node.ranges.clear(); + node.ranges.add().init(-2,-1); // invalid range + BOOST_CHECK_THROW(node.validateRange(0), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(1), json_strict_exception); + BOOST_CHECK_NO_THROW(node.validateRange(2)); + BOOST_CHECK_NO_THROW(node.validateRange(3)); + + node.ranges.clear(); + node.ranges.add().init(3,1); // invalid range + BOOST_CHECK_THROW(node.validateRange(0), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(1), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(2), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(3), json_strict_exception); + BOOST_CHECK_THROW(node.validateRange(4), json_strict_exception); + } + + { // isNextEmpty + Firebird::AutoPtr first = new PathNode(pool); + BOOST_CHECK(first->isNextEmpty()); // the next is missing + + PathNode* middle = first->add(); + BOOST_CHECK(!first->isNextEmpty()); // the next is `middle` + + middle->disable(); + BOOST_CHECK(first->isNextEmpty()); // the next is missing (disabled) + + PathNode* last = middle->add(); + BOOST_CHECK(!first->isNextEmpty()); // the next is `last` + + BOOST_CHECK(last->isNextEmpty()); + } + + { // equals + Firebird::AutoPtr first = new PathNode(pool); + PathNode* middle = first->add(); + + JsonLevelNode jsonNode(pool); + + BOOST_CHECK(!first->equalsNoComplex(&jsonNode)); + BOOST_CHECK(first->matched == false); + + BOOST_CHECK(!middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == false); + + first->matched = true; + BOOST_CHECK(!middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == false); + + jsonNode.depth = middle->depth; + jsonNode.itemType = ItemType::FIELD; + jsonNode.field = "hello"; + middle->type = ItemType::FIELD; + middle->field = "123"; + BOOST_CHECK(!middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == false); + + middle->field = "hello"; + BOOST_CHECK(middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == true); + + PathNode* last = middle->add(); + BOOST_CHECK(!middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == true); + + last->disable(); + BOOST_CHECK(middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == true); + + jsonNode.depth++; + BOOST_CHECK(!middle->equalsNoComplex(&jsonNode)); + BOOST_CHECK(middle->matched == true); + } + + { // equals field with unwrap + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + middle->type = ItemType::FIELD; + + JsonLevelNode jsonNode(pool); + jsonNode.depth = middle->depth; + jsonNode.indexInArray = 2; + jsonNode.itemType = ItemType::ARRAY_ELEMENT; + + BOOST_CHECK(middle->equalsNoComplex(&jsonNode) == false); + BOOST_CHECK(!middle->matched); + + first->flags |= PathNode::FLAG_UNWRAP; + BOOST_CHECK(middle->equalsNoComplex(&jsonNode) == false); + BOOST_CHECK(!middle->matched); + BOOST_CHECK(first->next != middle); + BOOST_CHECK(first->next->next == middle); + BOOST_CHECK(first->next->state == PathNodeState::TEMPORARY); + BOOST_CHECK(first->next->matched); + } + + { // equals array + Firebird::AutoPtr first = new PathNode(pool); + first->matched = true; + PathNode* middle = first->add(); + + JsonLevelNode jsonNode(pool); + jsonNode.depth = middle->depth; + jsonNode.indexInArray = 2; + jsonNode.itemType = ItemType::ARRAY_ELEMENT; + + middle->ranges.add().init(0, 1); + middle->type = ItemType::ARRAY_ELEMENT; + middle->equalsNoComplex(&jsonNode); + BOOST_CHECK(!middle->matched); + + middle->ranges.add().init(1, 2); + middle->equalsNoComplex(&jsonNode); + BOOST_CHECK(middle->matched); + + jsonNode.itemType = ItemType::FIELD; + middle->equalsNoComplex(&jsonNode); + BOOST_CHECK(!middle->matched); + + // $[0].a + // {"a":42} + jsonNode.itemType = ItemType::FIELD; + jsonNode.field = "123"; + + middle->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(middle->equalsNoComplex(&jsonNode) == false); // not working without a next node + BOOST_CHECK(!middle->matched); + + auto last = middle->add(); + last->field = "123"; + last->type = ItemType::FIELD; + jsonNode.depth = 1; + middle->matched = false; + last->matched = false; + + BOOST_CHECK(middle->equalsNoComplex(&jsonNode) == false); // Check the wrapping level + BOOST_CHECK(middle->matched); + BOOST_CHECK(last->matched); + + last->add(); + middle->matched = false; + last->matched = false; + BOOST_CHECK(middle->equalsNoComplex(&jsonNode) == false); // last->next is not empty + BOOST_CHECK(middle->matched); + BOOST_CHECK(last->matched); + } + + { + Firebird::AutoPtr first = new PathNode(pool); + + JsonLevelNode jsonNode(pool); + + first->depth = 0; + jsonNode.depth = 0; + BOOST_CHECK(first->equalsZeroDepth(&jsonNode)); + + first->depth = 0; + jsonNode.depth = 1; + BOOST_CHECK(!first->equalsZeroDepth(&jsonNode)); + + first->depth = 1; + jsonNode.depth = 0; + BOOST_CHECK(!first->equalsZeroDepth(&jsonNode)); + + first->depth = 1; + jsonNode.depth = 1; + BOOST_CHECK(!first->equalsZeroDepth(&jsonNode)); + + } + + { // canWrap: Example: for {"field":42}, we can convert $[*].field to $.field + Firebird::AutoPtr first = new PathNode(pool); + + JsonLevelNode jsonNode(pool); + jsonNode.indexInArray = 2; + jsonNode.itemType = ItemType::ARRAY_ELEMENT; + + first->matched = false; + jsonNode.depth = 2; + first->flags = 0; + BOOST_CHECK(!first->canWrap(&jsonNode)); + + first->matched = true; + jsonNode.depth = 2; + first->flags = 0; + BOOST_CHECK(!first->canWrap(&jsonNode)); + + first->matched = true; + jsonNode.depth = 0; + first->flags = 0; + BOOST_CHECK(!first->canWrap(&jsonNode)); + + first->matched = true; + jsonNode.depth = 0; + first->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(!first->canWrap(&jsonNode)); + + first->add(); + BOOST_CHECK(!first->canWrap(&jsonNode)); + first->next->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(first->canWrap(&jsonNode)); + + first->matched = false; + jsonNode.depth = 0; + first->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(!first->canWrap(&jsonNode)); + + first->matched = true; + jsonNode.depth = 2; + first->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(!first->canWrap(&jsonNode)); + } + + { // canWrapEnd: Example: for '{"data":1}' we can convert '$.data[0].a to $.data + Firebird::AutoPtr first = new PathNode(pool); + + JsonLevelNode jsonNode(pool); + jsonNode.indexInArray = 2; + jsonNode.itemType = ItemType::ARRAY_ELEMENT; + + first->matched = true; + jsonNode.depth = 0; + + auto next = first->add(); + next->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(first->canWrap(&jsonNode)); // Walidate wrap is valid + BOOST_CHECK(first->canWrapEnd(&jsonNode)); + next->add(); + BOOST_CHECK(!first->canWrapEnd(&jsonNode)); + } + + + { // canUnwrapEnd + Firebird::AutoPtr first = new PathNode(pool); + BOOST_CHECK(!first->canUnwrapEnd()); + + first->matched = false; + first->flags |= PathNode::FLAG_UNWRAP; + BOOST_CHECK(!first->canUnwrapEnd()); + + first->matched = true; + first->flags |= PathNode::FLAG_UNWRAP; + BOOST_CHECK(first->canUnwrapEnd()); + + first->matched = true; + first->flags = 0; + BOOST_CHECK(!first->canUnwrapEnd()); + + first->matched = true; + first->flags |= PathNode::FLAG_UNWRAP; + first->add(); + BOOST_CHECK(!first->canUnwrapEnd()); + } + + + { // canUnwrapMiddle: сan insert [*] to path + Firebird::AutoPtr first = new PathNode(pool); + + JsonLevelNode jsonNode(pool); + jsonNode.indexInArray = 2; + + first->matched = false; + first->flags = 0; + jsonNode.type = FBJSON::JT_OBJECT; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = false; + first->flags = 0; + jsonNode.type = FBJSON::JT_ARRAY; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = false; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_OBJECT; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = false; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_ARRAY; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = true; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_OBJECT; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = true; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_ARRAY; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->add(); // next + + first->matched = false; + first->flags = 0; + jsonNode.type = FBJSON::JT_OBJECT; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = false; + first->flags = 0; + jsonNode.type = FBJSON::JT_ARRAY; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = false; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_OBJECT; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = false; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_ARRAY; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = true; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_OBJECT; + BOOST_CHECK(!first->canUnwrapMiddle(&jsonNode)); + + first->matched = true; + first->flags |= PathNode::FLAG_UNWRAP; + jsonNode.type = FBJSON::JT_ARRAY; + BOOST_CHECK(first->canUnwrapMiddle(&jsonNode)); + } + + { // canUnwrapEnd + Firebird::AutoPtr first = new PathNode(pool); + + BOOST_CHECK(!first->matchWrapPattern()); + BOOST_CHECK(!first->matchUnwrapPattern()); + + first->flags |= PathNode::FLAG_UNWRAP; + BOOST_CHECK(!first->matchWrapPattern()); + BOOST_CHECK(first->matchUnwrapPattern()); + + first->flags = 0; + first->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(first->matchWrapPattern()); + BOOST_CHECK(!first->matchUnwrapPattern()); + + first->flags |= PathNode::FLAG_WRAP; + first->flags |= PathNode::FLAG_UNWRAP; + BOOST_CHECK(first->matchWrapPattern()); + BOOST_CHECK(first->matchUnwrapPattern()); + } + + { // getEnabledPrev + Firebird::AutoPtr first = new PathNode(pool); + PathNode* middle = first->add(); + PathNode* last = middle->add(); + + BOOST_CHECK(first->getEnabledPrev() == nullptr); + BOOST_CHECK(middle->getEnabledPrev() == first); + BOOST_CHECK(last->getEnabledPrev() == middle); + + middle->disable(); + BOOST_CHECK(last->getEnabledPrev() == first); + + first->disable(); + BOOST_CHECK(last->getEnabledPrev() == nullptr); + } + + { // isEmpty + PathNode node(pool); + BOOST_CHECK(node.isEmpty(0)); + BOOST_CHECK(node.isEmpty(PathNode::FLAG_UNWRAP)); + node.add(); + BOOST_CHECK(node.isEmpty(0) == false); + } + + { // isEmpty + PathNode node(pool); + node.flags |= (PathNode::FLAG_UNWRAP | PathNode::FLAG_WRAP); + BOOST_CHECK(node.isEmpty(0)); + BOOST_CHECK(node.isEmpty(PathNode::FLAG_UNWRAP) == false); + BOOST_CHECK(node.isEmpty(PathNode::FLAG_WRAP) == false); + BOOST_CHECK(node.isEmpty(PathNode::FLAG_UNWRAP | PathNode::FLAG_WRAP) == false); + BOOST_CHECK(node.isEmpty(PathNode::FLAG_PROCESSED) == true); + } + + { // isEmpty + PathNode node(pool); + node.filterNode = makeDummyNode(); + BOOST_CHECK(node.isEmpty(PathNode::FLAG_UNWRAP) == false); + } +} + + +BOOST_AUTO_TEST_SUITE_END() // PathNodeTests + + +BOOST_AUTO_TEST_SUITE(JsonPathsTests) + +BOOST_AUTO_TEST_CASE(JsonPathTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + { + JsonPath path(pool, false); + BOOST_CHECK(path.getRootNode() == nullptr); + } + + { + JsonPath path(pool, true); + BOOST_CHECK(path.getRootNode() != nullptr); + BOOST_CHECK(path.isZeroPath()); + } + + { + JsonPath path(pool, true); + BOOST_CHECK(!path.hasFields()); + path.setFieldsFlag(); + BOOST_CHECK(path.hasFields()); + } + + { + JsonPath path(pool, true); + BOOST_CHECK(!path.hasIndexes()); + path.setIndexesFlag(); + BOOST_CHECK(path.hasIndexes()); + } + + { + JsonPath path(pool, true); + BOOST_CHECK(!path.hasComplexRange()); + path.setComplexRangeFlag(); + BOOST_CHECK(path.hasComplexRange()); + } + + { + JsonPath path(pool, true); + BOOST_CHECK(path.isLax()); + BOOST_CHECK(!path.isStrict()); + path.setStrictFlag(); + BOOST_CHECK(!path.isLax()); + BOOST_CHECK(path.isStrict()); + } + + { + JsonPath path(pool, true); + BOOST_CHECK(!path.isWrappingZeroPath()); + auto* second = path.getRootNode()->add(); + BOOST_CHECK(!path.isWrappingZeroPath()); + second->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(path.isWrappingZeroPath()); + + second->add(); + BOOST_CHECK(!path.isWrappingZeroPath()); + } + + { + JsonPath path(pool, false); + BOOST_CHECK(path.isEmpty(0)); + auto node = path.resetRootNode(); + BOOST_CHECK(path.isEmpty(0)); + + node->flags |= PathNode::FLAG_WRAP; + BOOST_CHECK(path.isEmpty(0)); + BOOST_CHECK(!path.isEmpty(PathNode::FLAG_WRAP)); + + node->add(); + BOOST_CHECK(!path.isEmpty(0)); + } + + { + BOOST_CHECK(!JsonPath::isUnwrapMethod(PathMethod::TYPE)); + BOOST_CHECK(!JsonPath::isUnwrapMethod(PathMethod::NONE)); + BOOST_CHECK(!JsonPath::isUnwrapMethod(PathMethod::SIZE)); + for (ULONG i = ULONG(PathMethod::SIZE) + 1; i < ULONG(PathMethod::TIMESTAMP_TZ); ++i) + { + BOOST_CHECK(JsonPath::isUnwrapMethod(PathMethod(i))); + } + } +} + +BOOST_AUTO_TEST_CASE(JsonPathExprTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + JsonPathExpr expr(pool); + BOOST_CHECK(expr.getJsonPath() != nullptr); + + auto a = JsonExprNode::make(pool); + auto b = JsonExprNode::make(pool); + expr.resetExpr(a, b); + + BOOST_CHECK(expr.getTail() == a); + BOOST_CHECK(expr.getMath() == b); + + // JsonPathExpr PathVariable + + a = new JsonExprNode(pool, new PathVariable(pool, PathVariable::Type::ROOT)); + b = new JsonExprNode(pool, new PathVariable(pool, PathVariable::Type::ROOT)); + expr.resetExpr(a, b); + BOOST_CHECK(expr.getTail() == nullptr); // Empty because ROOT + BOOST_CHECK(expr.getMath() == nullptr); + + a = new JsonExprNode(pool, new PathVariable(pool, PathVariable::Type::ITEM)); + b = new JsonExprNode(pool, new PathVariable(pool, PathVariable::Type::ITEM)); + expr.resetExpr(a, b); + BOOST_CHECK(expr.getTail() == nullptr); // Empty because isRootEmpty + BOOST_CHECK(expr.getMath() != nullptr); + + b = new JsonExprNode(pool, new PathVariable(pool, PathVariable::Type::ITEM)); + expr.resetExpr(nullptr, b); + BOOST_CHECK(expr.getTail() == nullptr); // Empty because isRootEmpty + BOOST_CHECK(expr.getMath() != nullptr); +} + +BOOST_AUTO_TEST_CASE(PathVariableTest) +{ + MemoryPool& pool = *getDefaultMemoryPool(); + + { + PathVariable var(pool); + BOOST_CHECK(!var.hasPath(0)); + + var.path = new JsonPath(pool, true); + BOOST_CHECK(!var.hasPath(0)); + + var.path->getRootNode()->flags |= PathNode::FLAG_UNWRAP; + BOOST_CHECK(var.hasPath(PathNode::FLAG_UNWRAP)); + BOOST_CHECK(!var.hasPath(0)); + } + + { + PathVariable var(pool, PathVariable::Type::JSON); + BOOST_CHECK(!var.isItem()); + BOOST_CHECK(!var.isPassing()); + } + + { + PathVariable var(pool, PathVariable::Type::ITEM); + BOOST_CHECK(var.isItem()); + BOOST_CHECK(!var.isPassing()); + } + + { + PathVariable var(pool, PathVariable::Type::PASSING); + BOOST_CHECK(!var.isItem()); + BOOST_CHECK(var.isPassing()); + } + + { + PathVariable var(pool, PathVariable::Type::ROOT); + BOOST_CHECK(!var.isItem()); + BOOST_CHECK(!var.isPassing()); + } + + { + PathVariable var(pool, PathVariable::Type::HEAD); + BOOST_CHECK(!var.isItem()); + BOOST_CHECK(!var.isPassing()); + } +} + +BOOST_AUTO_TEST_SUITE_END() // JsonPathClassTests + +BOOST_AUTO_TEST_SUITE_END() // JsonClassesTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite diff --git a/src/jrd/tests/json/classes/JsonScalarTest.cpp b/src/jrd/tests/json/classes/JsonScalarTest.cpp new file mode 100644 index 00000000000..de6063a0bdc --- /dev/null +++ b/src/jrd/tests/json/classes/JsonScalarTest.cpp @@ -0,0 +1,376 @@ +#include "boost/test/unit_test.hpp" +#include "../../TestContext.h" + +#include "../jrd/json/classes/JsonTypes.h" +#include "../jrd/json/classes/JsonScalar.h" +#include "../jrd/json/classes/JsonDatetime.h" +#include "../common/classes/BlrReader.h" +#include "../common/classes/BlrWriter.h" + +#include + +using namespace FBJSON; + + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(JsonClassesTests) + +BOOST_AUTO_TEST_SUITE(JsonScalarTests) + + +BOOST_AUTO_TEST_CASE(JsonScalarValues) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + + { // NULL CHECK + JsonScalar scalar(pool); + BOOST_CHECK(scalar.isNull()); + BOOST_CHECK(!scalar.isScalar()); + + scalar.set(true); + BOOST_CHECK(!scalar.isNull()); + BOOST_CHECK(scalar.isScalar()); + } + + { + JsonScalar scalar(pool); + scalar.setToNull(); + BOOST_CHECK(scalar.isNull()); + BOOST_CHECK(!scalar.isScalar()); + } + + { // BOOL CHECK + JsonScalar scalar(pool); + scalar.set(true); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::BOOL); + BOOST_TEST(scalar.getValue().boolean == true); + BOOST_TEST(scalar.isTrue()); + BOOST_CHECK(scalar.isScalar()); + } + + { // BOOL CHECK + JsonScalar scalar(pool); + scalar.set(false); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::BOOL); + BOOST_TEST(scalar.getValue().boolean == false); + BOOST_TEST(scalar.isTrue() == false); + BOOST_CHECK(scalar.isScalar()); + } + + { // INT CHECK + JsonScalar scalar(pool); + scalar.set(42); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::INT); + BOOST_TEST(scalar.getValue().integer == 42); + BOOST_TEST(scalar.isNumber() == true); + BOOST_TEST(scalar.getDouble() == 42); + BOOST_CHECK(scalar.isScalar()); + } + + { // DOUBLE CHECK + JsonScalar scalar(pool); + scalar.set(42.42); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::DOUBLE); + BOOST_TEST(scalar.getValue().doubleValue == 42.42); + BOOST_TEST(scalar.isNumber() == true); + BOOST_TEST(scalar.getDouble() == 42.42); + BOOST_CHECK(scalar.isScalar()); + } + + { // STRING CHECK + const char* testValue = "raw char pointer"; + + JsonScalar scalar(pool); + scalar.set(testValue); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::STRING); + BOOST_TEST(std::string_view(*scalar.getValue().string) == std::string_view(testValue)); + BOOST_CHECK(scalar.isString()); + BOOST_CHECK(scalar.isScalar()); + } + + { + std::string_view testValue = "string_view string"; + + JsonScalar scalar(pool); + scalar.set(testValue); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::STRING); + BOOST_TEST(std::string_view(*scalar.getValue().string) == testValue); + BOOST_CHECK(scalar.isString()); + BOOST_CHECK(scalar.isScalar()); + + // Reset the same value + scalar.set(*scalar.getValue().string); + } + + // DATETIME CHECK + + { + JsonScalar scalar(pool); + auto& dt = scalar.setToDatetime(); + BOOST_CHECK(scalar.getType() == FBJSON::ValueType::DATETIME); + BOOST_CHECK(*scalar.getValue().datetime == dt); + BOOST_CHECK(scalar.isScalar()); + } +} + +BOOST_AUTO_TEST_CASE(GetDoubleMethod) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + + JsonScalar scalar(pool); + scalar.setToNull(); + BOOST_CHECK_THROW(scalar.getDouble(), json_skippable_exception); + + scalar.set(false); + BOOST_CHECK_THROW(scalar.getDouble(), json_skippable_exception); + + scalar.set("123"); + BOOST_CHECK_THROW(scalar.getDouble(), json_skippable_exception); + + scalar.set(123); + BOOST_TEST(scalar.getDouble() == 123); + + scalar.set(42.42); + BOOST_TEST(scalar.getDouble() == 42.42); +} + + +BOOST_AUTO_TEST_CASE(StrMethod) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + + JsonScalar scalar(pool); + // scalar.setToNull(); + // BOOST_CHECK_THROW(scalar.str(), json_skippable_exception); + + scalar.set("123"); + BOOST_TEST(scalar.str() == scalar.getValue().string); + + BOOST_TEST(std::string_view(*scalar.str()) == "123"); +} + +BOOST_AUTO_TEST_CASE(GetStringViewMethod) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + JsonScalar scalar(pool); + // scalar.setToNull(); + // BOOST_CHECK_THROW(scalar.getStringView(), json_skippable_exception); + + scalar.set("123"); + BOOST_TEST(scalar.getStringView() == std::string_view(*scalar.getValue().string)); + BOOST_TEST(scalar.getStringView() == "123"); +} + +BOOST_AUTO_TEST_CASE(FlagsMethods) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + JsonScalar scalar(pool); + + BOOST_CHECK(!scalar.hasFlag(JsonScalar::FLAG_HAS_QUOTES)); + scalar.addFlag(FBJSON::JsonScalar::FLAG_HAS_QUOTES); + BOOST_CHECK(scalar.hasFlag(JsonScalar::FLAG_HAS_QUOTES)); +} + +class TestBlrBuffer : public Firebird::BlrWriter +{ +public: + TestBlrBuffer(MemoryPool& pool = *getDefaultMemoryPool()) : Firebird::BlrWriter(pool) + { } + + bool isVersion4() final + { + return true; + } + + Firebird::BlrReader& getReader() + { + return m_reader = Firebird::BlrReader(getBlrData().begin(), getBlrData().getCount()); + } + +private: + Firebird::BlrReader m_reader; +}; + + +static void writeReadBytes(JsonScalar& inOut) +{ + TestBlrBuffer buffer; + inOut.writeScalarAsBytes(buffer); + buffer.appendUChar(0); // dummy end + + inOut.setToNull(); + inOut.readScalarFromBytes(buffer.getReader()); +} + +BOOST_AUTO_TEST_CASE(ReadWritesScalarBytes) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + JsonScalar scalar(pool); + + scalar.setToNull(); + writeReadBytes(scalar); + BOOST_CHECK(scalar.isNull()); + + scalar.set(false); + writeReadBytes(scalar); + BOOST_CHECK(scalar.getType() == ValueType::BOOL); + BOOST_TEST(scalar.getValue().boolean == false); + + scalar.set(true); + writeReadBytes(scalar); + BOOST_CHECK(scalar.getType() == ValueType::BOOL); + BOOST_TEST(scalar.getValue().boolean == true); + + scalar.set(42); + writeReadBytes(scalar); + BOOST_CHECK(scalar.getType() == ValueType::INT); + BOOST_TEST(scalar.getValue().integer == 42); + + scalar.set(42.42); + writeReadBytes(scalar); + BOOST_CHECK(scalar.getType() == ValueType::DOUBLE); + BOOST_TEST(scalar.getValue().doubleValue == 42.42); + + scalar.set("12345"); + writeReadBytes(scalar); + BOOST_CHECK(scalar.isString()); + BOOST_TEST(scalar.getStringView() == "12345"); + + scalar.setToDatetime().set("2018-01-01"); + auto ts1 = scalar.getValue().datetime->getTS(); + writeReadBytes(scalar); + BOOST_CHECK(scalar.getType() == ValueType::DATETIME); + BOOST_TEST(scalar.getValue().datetime->asString() == "2018-01-01"); + BOOST_TEST(scalar.getValue().datetime->getTypeName() == "date"); + + auto ts2 = scalar.getValue().datetime->getTS(); + BOOST_CHECK(ts1.time_zone == ts2.time_zone); + BOOST_CHECK(ts1.utc_timestamp.timestamp_date == ts2.utc_timestamp.timestamp_date); + BOOST_CHECK(ts1.utc_timestamp.timestamp_time == ts2.utc_timestamp.timestamp_time); +} + +static void writeReadBlr(JsonScalar& inOut) +{ + TestBlrBuffer buffer; + inOut.writeScalarAsBytes(buffer); + buffer.appendUChar(0); // To fix assert with seekForward + + inOut.setToNull(); + + auto reader = buffer.getReader(); + inOut.readScalarFromBytes(reader); +} + +BOOST_AUTO_TEST_CASE(ReadWritesScalarBlr) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + JsonScalar scalar(pool); + + scalar.setToNull(); + writeReadBlr(scalar); + BOOST_CHECK(scalar.isNull()); + + scalar.set(false); + writeReadBlr(scalar); + BOOST_CHECK(scalar.getType() == ValueType::BOOL); + BOOST_TEST(scalar.getValue().boolean == false); + + scalar.set(true); + writeReadBlr(scalar); + BOOST_CHECK(scalar.getType() == ValueType::BOOL); + BOOST_TEST(scalar.getValue().boolean == true); + + scalar.set(42); + writeReadBlr(scalar); + BOOST_CHECK(scalar.getType() == ValueType::INT); + BOOST_TEST(scalar.getValue().integer == 42); + + scalar.set(42.42); + writeReadBlr(scalar); + BOOST_CHECK(scalar.getType() == ValueType::DOUBLE); + BOOST_TEST(scalar.getValue().doubleValue == 42.42); + + scalar.set("12345"); + scalar.addFlag(JsonScalar::FLAG_HAS_QUOTES); + writeReadBlr(scalar); + BOOST_CHECK(scalar.isString()); + BOOST_CHECK(!scalar.hasFlag(JsonScalar::FLAG_HAS_QUOTES)); // Only runtime + BOOST_TEST(scalar.getStringView() == "12345"); + + scalar.setToDatetime().set("2018-01-01"); + auto ts1 = scalar.getValue().datetime->getTS(); + writeReadBlr(scalar); + BOOST_CHECK(scalar.getType() == ValueType::DATETIME); + BOOST_TEST(scalar.getValue().datetime->asString() == "2018-01-01"); + BOOST_TEST(scalar.getValue().datetime->getTypeName() == "date"); + + auto ts2 = scalar.getValue().datetime->getTS(); + BOOST_CHECK(ts1.time_zone == ts2.time_zone); + BOOST_CHECK(ts1.utc_timestamp.timestamp_date == ts2.utc_timestamp.timestamp_date); + BOOST_CHECK(ts1.utc_timestamp.timestamp_time == ts2.utc_timestamp.timestamp_time); +} + +BOOST_AUTO_TEST_CASE(ScalarToDsc) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + JsonScalar scalar(pool); + Jrd::impure_value out; + + scalar.setToNull(); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.isNull()); + + scalar.set(false); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.isBoolean()); + BOOST_CHECK(reinterpret_cast(*out.vlu_desc.dsc_address) == false); + + scalar.set(true); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.isBoolean()); + BOOST_CHECK(reinterpret_cast(*out.vlu_desc.dsc_address) == true); + + scalar.set(42); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.dsc_dtype == dtype_int64); + BOOST_CHECK(reinterpret_cast(*out.vlu_desc.dsc_address) == 42); + + scalar.set(42.42); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.dsc_dtype == dtype_double); + BOOST_CHECK(reinterpret_cast(*out.vlu_desc.dsc_address) == 42.42); + + scalar.set("12345"); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.dsc_dtype == dtype_text); + + const std::string_view view(reinterpret_cast(out.vlu_desc.dsc_address), out.vlu_desc.getStringLength()); + BOOST_CHECK(view == "12345"); + + scalar.setToDatetime().set("2018-01-01"); + auto ts1 = scalar.getValue().datetime->getTS(); + out = scalar.makeScalarDsc(); + BOOST_CHECK(out.vlu_desc.dsc_dtype == dtype_timestamp_tz); + + auto ts2 = *reinterpret_cast(out.vlu_desc.dsc_address); + BOOST_CHECK(ts1.time_zone == ts2.time_zone); + BOOST_CHECK(ts1.utc_timestamp.timestamp_date == ts2.utc_timestamp.timestamp_date); + BOOST_CHECK(ts1.utc_timestamp.timestamp_time == ts2.utc_timestamp.timestamp_time); +} + +BOOST_AUTO_TEST_CASE(StringViewDsc) +{ + Firebird::MemoryPool& pool = *getDefaultMemoryPool(); + + Firebird::string string = "123"; + + JsonScalar::StringView view(pool, &string); + + BOOST_REQUIRE((*view).isString()); + BOOST_REQUIRE((*view).getStringView() == "123"); +} + +BOOST_AUTO_TEST_SUITE_END() // JsonScalarTests + +BOOST_AUTO_TEST_SUITE_END() // JsonClassesTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite