From c9139060cf87d727ffad064227a8daab1eb80ca1 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 09:40:49 +0300 Subject: [PATCH 01/20] Implement JSON Path parser and related routines --- builds/posix/Makefile.in | 1 + builds/posix/make.shared.targets | 20 + builds/posix/make.shared.variables | 9 +- builds/win32/msvc15/engine_static.vcxproj | 18 + .../msvc15/engine_static.vcxproj.filters | 44 + builds/win32/parse.bat | 23 + doc/sql.extensions/README.json.md | 199 +++ src/CMakeLists.txt | 28 + src/common/CvtFormat.cpp | 60 +- src/common/CvtFormat.h | 3 +- src/common/TimeZoneUtil.cpp | 30 + src/common/TimeZoneUtil.h | 3 + src/common/classes/BlrReader.h | 18 +- src/common/classes/NoThrowTimeStamp.h | 22 + src/common/classes/fb_string.h | 5 + src/common/cvt.cpp | 17 +- src/common/cvt.h | 12 +- src/common/tests/CvtTestUtils.h | 14 +- src/dsql/make.cpp | 10 +- src/jrd/json/JsonConsts.h | 347 +++++ src/jrd/json/JsonRuntime.cpp | 298 +++++ src/jrd/json/JsonRuntime.h | 528 ++++++++ src/jrd/json/JsonUtils.h | 110 ++ src/jrd/json/classes/JsonDatetime.cpp | 338 +++++ src/jrd/json/classes/JsonDatetime.h | 138 ++ src/jrd/json/classes/JsonScalar.cpp | 253 ++++ src/jrd/json/classes/JsonScalar.h | 306 +++++ src/jrd/json/classes/JsonTypes.cpp | 462 +++++++ src/jrd/json/classes/JsonTypes.h | 312 +++++ src/jrd/json/path/JPathParser.cpp | 615 +++++++++ src/jrd/json/path/JPathParser.h | 498 ++++++++ src/jrd/json/path/JsonPath.cpp | 296 +++++ src/jrd/json/path/JsonPath.h | 699 +++++++++++ src/jrd/json/path/JsonPathParser.y | 799 ++++++++++++ src/jrd/json/path/btyacc_json.ske | 743 +++++++++++ src/jrd/json/path/jpath-parse-conflicts.txt | 1 + src/jrd/tests/TestContext.cpp | 81 ++ src/jrd/tests/TestContext.h | 23 + src/jrd/tests/json/DecimalConvert.cpp | 106 ++ src/jrd/tests/json/JsonExprNodeTest.cpp | 793 ++++++++++++ src/jrd/tests/json/JsonPathParsingTest.cpp | 452 +++++++ src/jrd/tests/json/JsonTestUtils.h | 108 ++ .../tests/json/classes/JsonDatetimeTest.cpp | 374 ++++++ src/jrd/tests/json/classes/JsonPathTest.cpp | 1112 +++++++++++++++++ src/jrd/tests/json/classes/JsonScalarTest.cpp | 376 ++++++ 45 files changed, 10681 insertions(+), 23 deletions(-) create mode 100644 doc/sql.extensions/README.json.md create mode 100644 src/jrd/json/JsonConsts.h create mode 100644 src/jrd/json/JsonRuntime.cpp create mode 100644 src/jrd/json/JsonRuntime.h create mode 100644 src/jrd/json/JsonUtils.h create mode 100644 src/jrd/json/classes/JsonDatetime.cpp create mode 100644 src/jrd/json/classes/JsonDatetime.h create mode 100644 src/jrd/json/classes/JsonScalar.cpp create mode 100644 src/jrd/json/classes/JsonScalar.h create mode 100644 src/jrd/json/classes/JsonTypes.cpp create mode 100644 src/jrd/json/classes/JsonTypes.h create mode 100644 src/jrd/json/path/JPathParser.cpp create mode 100644 src/jrd/json/path/JPathParser.h create mode 100644 src/jrd/json/path/JsonPath.cpp create mode 100644 src/jrd/json/path/JsonPath.h create mode 100644 src/jrd/json/path/JsonPathParser.y create mode 100644 src/jrd/json/path/btyacc_json.ske create mode 100644 src/jrd/json/path/jpath-parse-conflicts.txt create mode 100644 src/jrd/tests/TestContext.cpp create mode 100644 src/jrd/tests/TestContext.h create mode 100644 src/jrd/tests/json/DecimalConvert.cpp create mode 100644 src/jrd/tests/json/JsonExprNodeTest.cpp create mode 100644 src/jrd/tests/json/JsonPathParsingTest.cpp create mode 100644 src/jrd/tests/json/JsonTestUtils.h create mode 100644 src/jrd/tests/json/classes/JsonDatetimeTest.cpp create mode 100644 src/jrd/tests/json/classes/JsonPathTest.cpp create mode 100644 src/jrd/tests/json/classes/JsonScalarTest.cpp 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..935b5a01f8e 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -61,6 +61,26 @@ $(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 > $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt + 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..beb2d846e87 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -200,6 +200,14 @@ + + + + + + + + @@ -402,6 +410,13 @@ + + + + + + + @@ -422,6 +437,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..5c0acab5178 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,46 @@ DSQL + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JRD files\Json + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + JSON + diff --git a/builds/win32/parse.bat b/builds/win32/parse.bat index e2ea85fd632..30a4afcbcd9 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\jrd\json\path\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..61c9be808b3 --- /dev/null +++ b/src/jrd/json/path/JsonPathParser.y @@ -0,0 +1,799 @@ +%{ +/* + * 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..f619c97d209 --- /dev/null +++ b/src/jrd/tests/json/DecimalConvert.cpp @@ -0,0 +1,106 @@ +#include "firebird.h" +#include "boost/test/unit_test.hpp" +#include "../jrd/json/JsonUtils.h" + +using namespace Firebird; +#define CI_SKIP return; + + +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) +{ + 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) +{ + CI_SKIP + + 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 int intNumber = 2147483646; + + FBJSON::NumberConvertBuffer buffer; + BOOST_TEST(FBJSON::convertNumberToString(buffer, intNumber) == "2147483646"); +} + +BOOST_AUTO_TEST_CASE(bigintTest) +{ + const unsigned long long 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..00fe47d097a --- /dev/null +++ b/src/jrd/tests/json/JsonTestUtils.h @@ -0,0 +1,108 @@ +#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" + +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 From 71b033932721c4db1aaae8bc31635b0c3c09c12d Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 10:10:52 +0300 Subject: [PATCH 02/20] Do not run locale-depended JSON unit tests --- src/jrd/tests/json/DecimalConvert.cpp | 6 +++--- src/jrd/tests/json/JsonTestUtils.h | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/jrd/tests/json/DecimalConvert.cpp b/src/jrd/tests/json/DecimalConvert.cpp index f619c97d209..29c8b3fdf6e 100644 --- a/src/jrd/tests/json/DecimalConvert.cpp +++ b/src/jrd/tests/json/DecimalConvert.cpp @@ -1,9 +1,9 @@ #include "firebird.h" #include "boost/test/unit_test.hpp" #include "../jrd/json/JsonUtils.h" +#include "JsonTestUtils.h" using namespace Firebird; -#define CI_SKIP return; class LocaleChangeHolder @@ -38,6 +38,7 @@ constexpr double NUMBER_TO_CONVERT = 3.1415926535'8979323846'2643383279'50288419 BOOST_AUTO_TEST_CASE(ruUtf8LocaleTest) { + SKIP_TEST; // Missing locale LocaleChangeHolder ruEncoding("ru_RU.UTF-8"); FBJSON::NumberConvertBuffer buffer; @@ -54,8 +55,7 @@ BOOST_AUTO_TEST_CASE(enUtf8LocaleTest) BOOST_AUTO_TEST_CASE(russianLocaleTest) { - CI_SKIP - + SKIP_TEST // Missing locale LocaleChangeHolder ruEncoding("russian"); FBJSON::NumberConvertBuffer buffer; diff --git a/src/jrd/tests/json/JsonTestUtils.h b/src/jrd/tests/json/JsonTestUtils.h index 00fe47d097a..614ccb49803 100644 --- a/src/jrd/tests/json/JsonTestUtils.h +++ b/src/jrd/tests/json/JsonTestUtils.h @@ -10,6 +10,14 @@ #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 From 554244cb5e73410f779797c9162a81c5ced254ee Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 10:11:16 +0300 Subject: [PATCH 03/20] Correct JSON files in engine vcxproj --- builds/win32/msvc15/engine_static.vcxproj | 5 ++- .../msvc15/engine_static.vcxproj.filters | 33 +++---------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/builds/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj index beb2d846e87..7978923259f 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -206,8 +206,8 @@ - + @@ -411,12 +411,11 @@ - - + diff --git a/builds/win32/msvc15/engine_static.vcxproj.filters b/builds/win32/msvc15/engine_static.vcxproj.filters index 5c0acab5178..53f3935906b 100644 --- a/builds/win32/msvc15/engine_static.vcxproj.filters +++ b/builds/win32/msvc15/engine_static.vcxproj.filters @@ -1217,46 +1217,23 @@ DSQL - - JSON - - - JSON - - - JSON - - - JSON - JSON - + JSON - + JSON - - JRD files\Json - - - JSON - - - JSON - - + JSON - + JSON JSON + - - JSON - From 5a1c3d6b637b194b311615c44dd98d366c047d73 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 10:46:09 +0300 Subject: [PATCH 04/20] Copy jpath-parse-conflicts.txt from temp to src In docker build, the src is not available. So store it in temp dir and then try to copy to the src/jrd/json dir --- builds/posix/make.shared.targets | 6 +++++- builds/win32/parse.bat | 2 +- src/jrd/json/path/JsonPathParser.y | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index 935b5a01f8e..14d7f3dab28 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -70,7 +70,11 @@ $(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_RO 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 > $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt + 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; \ + 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 diff --git a/builds/win32/parse.bat b/builds/win32/parse.bat index 30a4afcbcd9..1a1e92c803f 100644 --- a/builds/win32/parse.bat +++ b/builds/win32/parse.bat @@ -39,7 +39,7 @@ @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\jrd\json\path\jpath-parse-conflicts.txt +@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 diff --git a/src/jrd/json/path/JsonPathParser.y b/src/jrd/json/path/JsonPathParser.y index 61c9be808b3..b23b4b4fc19 100644 --- a/src/jrd/json/path/JsonPathParser.y +++ b/src/jrd/json/path/JsonPathParser.y @@ -230,6 +230,7 @@ path_accessor_compiled } ; + // ['-'] %type json_path_expr json_path_expr From cc27f99327945d0016cf9b97accf2e7dc54214e5 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 16:23:25 +0300 Subject: [PATCH 05/20] Allow fail when coping jpath-parse-conflicts.txt to src/jrd/json/path dir --- builds/posix/make.shared.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index 14d7f3dab28..ca951f91274 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -73,7 +73,7 @@ $(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_RO 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; \ + 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 From d7403463851a20fcaea5ef9173e35b8eaaf9d493 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 16:24:38 +0300 Subject: [PATCH 06/20] Use accurate int types in JSON decimal convert tests --- src/jrd/tests/json/DecimalConvert.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/jrd/tests/json/DecimalConvert.cpp b/src/jrd/tests/json/DecimalConvert.cpp index 29c8b3fdf6e..2364381f07a 100644 --- a/src/jrd/tests/json/DecimalConvert.cpp +++ b/src/jrd/tests/json/DecimalConvert.cpp @@ -88,15 +88,23 @@ BOOST_AUTO_TEST_CASE(doubleTest) BOOST_AUTO_TEST_CASE(intTest) { - const int intNumber = 2147483646; + 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 unsigned long long bigintNumber = 9223372037; + const u_int64_t bigintNumber = 9223372037; FBJSON::NumberConvertBuffer buffer; BOOST_TEST(FBJSON::convertNumberToString(buffer, bigintNumber) == "9223372037"); From d3fc83e9c2be7fae77b04e549d7ee1519e530d30 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 15 Jun 2026 16:42:31 +0300 Subject: [PATCH 07/20] Fix incorrect syntax in jpath-parse-conflicts.txt copy --- builds/posix/make.shared.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index ca951f91274..f997f75d9e8 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -73,7 +73,7 @@ $(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_RO 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 \ + 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 From 3cd1a0414e44b370a872880dd60f3a108feda820 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 22 Jun 2026 12:05:35 +0300 Subject: [PATCH 08/20] THis should fix JSON build for most systems --- builds/posix/make.shared.targets | 4 ++-- src/jrd/json/JsonRuntime.h | 2 +- src/jrd/json/JsonUtils.h | 33 ++++----------------------- src/jrd/json/path/JsonPath.cpp | 5 ++++ src/jrd/json/path/JsonPath.h | 7 +----- src/jrd/tests/json/DecimalConvert.cpp | 2 +- 6 files changed, 15 insertions(+), 38 deletions(-) diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index f997f75d9e8..ad8e8962cb7 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -73,11 +73,11 @@ $(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_RO 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; \ + cp $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt 2>/dev/null || : ; \ 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.h $(TMP_ROOT)/include/gen/jparse.h $(MV) $(GEN_ROOT)/jpath_tab.c $(OBJ)/jrd/json/path/JsonPathParser.cpp touch $(OBJ)/jrd/json/path/JsonPathParser.cpp diff --git a/src/jrd/json/JsonRuntime.h b/src/jrd/json/JsonRuntime.h index 31a8cfddf37..08bdea3e648 100644 --- a/src/jrd/json/JsonRuntime.h +++ b/src/jrd/json/JsonRuntime.h @@ -31,6 +31,7 @@ #include "JsonConsts.h" #include "classes/JsonScalar.h" #include "classes/JsonTypes.h" +#include "path/JsonPath.h" namespace FBJSON { @@ -84,7 +85,6 @@ enum class JsonExprOperation : UCHAR }; class PassingList; -struct PathVariable; // * * * * diff --git a/src/jrd/json/JsonUtils.h b/src/jrd/json/JsonUtils.h index 054be3401ad..797292e93a3 100644 --- a/src/jrd/json/JsonUtils.h +++ b/src/jrd/json/JsonUtils.h @@ -49,37 +49,15 @@ 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 '.' + // 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 + // Do not use begin/end or front/back because they requeue some obscure extra casts to char* on Windows + const char* end = buffer.data() + buffer.size(); if constexpr (std::is_floating_point_v) - result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value, std::chars_format::general, NUMBER_PRECISION); + result = std::to_chars(buffer.data(), end, value, std::chars_format::general, NUMBER_PRECISION); else - result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + result = std::to_chars(buffer.data(), end, value); if (result.ec != std::errc()) { @@ -88,7 +66,6 @@ inline std::string_view convertNumberToString(NumberConvertBuffer& buffer, T val } return std::string_view(buffer.data(), result.ptr - buffer.data()); -#endif } template diff --git a/src/jrd/json/path/JsonPath.cpp b/src/jrd/json/path/JsonPath.cpp index dd77e89c84b..82ac1ba5c0b 100644 --- a/src/jrd/json/path/JsonPath.cpp +++ b/src/jrd/json/path/JsonPath.cpp @@ -294,3 +294,8 @@ bool PathVariable::hasPath(const UCHAR flagsMask) const { return path.hasData() && !path->isEmpty(flagsMask); } + + +PathInjection::PathInjection(MemoryPool& pool) + : Firebird::PermanentStorage(pool) +{ } diff --git a/src/jrd/json/path/JsonPath.h b/src/jrd/json/path/JsonPath.h index 3c1da52303f..0cf4e57335b 100644 --- a/src/jrd/json/path/JsonPath.h +++ b/src/jrd/json/path/JsonPath.h @@ -39,8 +39,6 @@ inline constexpr PathArrayIndex JPATH_ARRAY_LAST_INDEX = -1; struct PathNode; class JsonExprNode; class JsonPath; -class PathParser; -class PassingList; struct PathVariable; struct PathInjection; @@ -681,10 +679,7 @@ struct PathInjection : private Firebird::PermanentStorage // What to inject Firebird::AutoPtr expr; - PathInjection(MemoryPool& pool) - : Firebird::PermanentStorage(pool) - { } - + PathInjection(MemoryPool& pool); PathInjection(PathInjection&&) noexcept = delete; PathInjection(const PathInjection&) = delete; diff --git a/src/jrd/tests/json/DecimalConvert.cpp b/src/jrd/tests/json/DecimalConvert.cpp index 2364381f07a..027b09daacb 100644 --- a/src/jrd/tests/json/DecimalConvert.cpp +++ b/src/jrd/tests/json/DecimalConvert.cpp @@ -104,7 +104,7 @@ BOOST_AUTO_TEST_CASE(NegativeintTest) } BOOST_AUTO_TEST_CASE(bigintTest) { - const u_int64_t bigintNumber = 9223372037; + const uint64_t bigintNumber = 9223372037; FBJSON::NumberConvertBuffer buffer; BOOST_TEST(FBJSON::convertNumberToString(buffer, bigintNumber) == "9223372037"); From 00ead56635b6e534c5af2e277fd808b275e5add4 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 22 Jun 2026 12:27:12 +0300 Subject: [PATCH 09/20] Remove not existing json file from engine_static --- builds/win32/msvc15/engine_static.vcxproj | 1 - 1 file changed, 1 deletion(-) diff --git a/builds/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj index acf1be310d9..76096a593bf 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -201,7 +201,6 @@ - From e1d2982c9c9ccb78af186e4333439ee391d3d91d Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 22 Jun 2026 12:28:12 +0300 Subject: [PATCH 10/20] Use non constant char for to_chars to fix builds --- src/jrd/json/JsonUtils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jrd/json/JsonUtils.h b/src/jrd/json/JsonUtils.h index 797292e93a3..3d506ffb825 100644 --- a/src/jrd/json/JsonUtils.h +++ b/src/jrd/json/JsonUtils.h @@ -53,7 +53,7 @@ inline std::string_view convertNumberToString(NumberConvertBuffer& buffer, T val std::to_chars_result result; // Do not use begin/end or front/back because they requeue some obscure extra casts to char* on Windows - const char* end = buffer.data() + buffer.size(); + char* end = buffer.data() + buffer.size(); if constexpr (std::is_floating_point_v) result = std::to_chars(buffer.data(), end, value, std::chars_format::general, NUMBER_PRECISION); else From 4ae699c17a6af60166b2eb4b204db5742ebad487 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 22 Jun 2026 12:28:44 +0300 Subject: [PATCH 11/20] Add missing `include` for json tokens and namespace for some constants --- src/jrd/json/JsonConsts.h | 51 +++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/jrd/json/JsonConsts.h b/src/jrd/json/JsonConsts.h index 6b9ad9d6207..25f8cf84430 100644 --- a/src/jrd/json/JsonConsts.h +++ b/src/jrd/json/JsonConsts.h @@ -36,22 +36,21 @@ #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 + inline constexpr TTypeId JSON_TTYPE = ttype_utf8; + inline constexpr USHORT JSON_TTYPE_BYTES_BET_CHAR = 4; + inline constexpr USHORT JSON_DEFAULT_SIZE = 6000 * JSON_TTYPE_BYTES_BET_CHAR; + + using TextPos = FB_UINT64; + using TextLength = FB_UINT64; + + inline constexpr TextPos INVALID_TEXT_POS = MAX_UINT64; + inline constexpr TextLength JSON_MAX_REPORT_SIZE = 50; + using SmallString = Firebird::string; using JsonToken = std::string_view; using JsonArrayIndex = USHORT; @@ -74,7 +73,7 @@ namespace FBJSON // Basically the inversion - constexpr CompareResult operator*(CompareResult lhs, const bool invert) + inline constexpr CompareResult operator*(CompareResult lhs, const bool invert) { if (!invert) return lhs; @@ -96,20 +95,20 @@ namespace FBJSON { 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"; + inline constexpr JsonToken EMPTY = "empty"; + inline constexpr JsonToken STRING = "string"; + inline constexpr JsonToken NUMBER = "number"; + inline constexpr JsonToken BOOLEAN = "boolean"; + inline constexpr JsonToken JNULL = "null"; + inline constexpr JsonToken ARRAY = "array"; + inline constexpr JsonToken OBJECT = "object"; + inline constexpr JsonToken DATETIME = "datetime"; + inline constexpr JsonToken UNKNOWN = "unknown"; + inline constexpr JsonToken TIME_WITH_TZ = "time with time zone"; + inline constexpr JsonToken TIME_WITHOUT_TZ = "time without time zone"; + inline constexpr JsonToken TIMESTAMT_WITH_TZ = "timestamp with time zone"; + inline constexpr JsonToken TIMESTAMT_WITHOUT_TZ = "timestamp without time zone"; + inline constexpr JsonToken DATE = "date"; } const JsonToken ANY_FIELD("*"); From 58ae5ee5edfe34fe6f6fa6e7c430fcd09860b62d Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Tue, 21 Jul 2026 15:00:32 +0300 Subject: [PATCH 12/20] Improve string parsing performance --- src/common/classes/fb_string.h | 5 + src/jrd/json/JsonConsts.h | 18 ++ src/jrd/json/classes/JsonTypes.cpp | 294 +++++++++++--------------- src/jrd/json/classes/JsonTypes.h | 290 ++++++++++++++++++++++++- src/jrd/json/path/JPathParser.cpp | 2 +- src/jrd/tests/json/JsonParserTest.cpp | 236 +++++++++++++++++++++ 6 files changed, 667 insertions(+), 178 deletions(-) create mode 100644 src/jrd/tests/json/JsonParserTest.cpp diff --git a/src/common/classes/fb_string.h b/src/common/classes/fb_string.h index 6366a1a6c13..998f35c5d5d 100644 --- a/src/common/classes/fb_string.h +++ b/src/common/classes/fb_string.h @@ -781,6 +781,11 @@ namespace Firebird append(1, c); return *this; } + StringType& operator+=(const std::string_view v) + { + append(v.data(), v.length()); + return *this; + } StringType operator+(const StringType& v) const { return add(v.c_str(), v.length()); diff --git a/src/jrd/json/JsonConsts.h b/src/jrd/json/JsonConsts.h index 25f8cf84430..4b07dd33d21 100644 --- a/src/jrd/json/JsonConsts.h +++ b/src/jrd/json/JsonConsts.h @@ -36,6 +36,24 @@ #include "../common/TextType.h" +#if defined(GNUC) || defined(clang) + #define FB_LIKELY(x) (__builtin_expect(!!(x), 1)) + #define FB_UNLIKELY(x) (__builtin_expect(!!(x), 0)) + #define FB_ASSUME(x) do { if (!(x)) __builtin_unreachable(); } while (0) + +#elif defined(_MSC_VER) + #define FB_LIKELY(x) (x) + #define FB_UNLIKELY(x) (x) + #define FB_ASSUME(x) __assume(x) + +#else + #define FB_LIKELY(x) (x) + #define FB_UNLIKELY(x) (x) + #define FB_ASSUME(x) ((void)0) + +#endif + + // Tokens namespace FBJSON { diff --git a/src/jrd/json/classes/JsonTypes.cpp b/src/jrd/json/classes/JsonTypes.cpp index ae0d1f69c6a..682aec3f46d 100644 --- a/src/jrd/json/classes/JsonTypes.cpp +++ b/src/jrd/json/classes/JsonTypes.cpp @@ -28,14 +28,14 @@ #include "../dsql/chars.h" #include +#include #ifdef HAVE_FLOAT_H #include #else -static inline constexpr DBL_MAX_10_EXP = 308 +static inline constexpr USHORT DBL_MAX_10_EXP = 308 #endif -#include "unicode/utf16.h" // U16_IS_TRAIL using namespace FBJSON; @@ -88,202 +88,164 @@ void json_fatal_exception::raise(const JsonStatusVector& statusVector) // Classes -template -void FBJSON::JsonScalarParser::parseQuotedString(TView& input, TextPos& current, SmallString& escapelessOutput) +void BasicParse::parseStringUnsafe(const std::string_view input, SmallString& output) { - escapelessOutput.clear(); - escapelessOutput.reserve(BUFFER_SMALL); - - const TextPos end = input.end(); + const ULONG length = input.length(); - // UTF-16, two \uXXXX in a row - int leadSurrogate = LEAD_SURROGATE_NULL; + output.clear(); + output.reserve(length); - while (current < end) + ULONG startPos = 0; + while (true) { - char c = input[current++]; - - if (c == '"') + // Use memchr because it is usually implemented with vector instructions + const char* slashIt = reinterpret_cast(std::memchr(input.data() + startPos, '\\', length - startPos)); + if (slashIt == nullptr) { + output += input.substr(startPos, length - startPos); return; } - else if ((unsigned char)c < 32) + + const auto currentPos = slashIt - input.data(); + output += input.substr(startPos, currentPos - startPos); // Get part before the escape char + + const char c = slashIt[1]; + if (c == 'u') { - escapelessOutput.printf("%02x", (unsigned char)c); - initError() << JsonStatusMsg(isc_jparser_unescaped_character) << escapelessOutput; + const auto unicodeSubstring = input.substr(currentPos); // \uXXXX + const auto unicodeValue = parseUnicodeUnsafe(unicodeSubstring.substr(ESCAPE_LENGTH)); + appendUnicode(unicodeValue.value, unicodeSubstring.substr(0, unicodeValue.length + ESCAPE_LENGTH), output); - escapelessOutput.clear(); - return; + startPos = currentPos + unicodeValue.length + ESCAPE_LENGTH; } - else if (c == '\\') + else { - if (current == end) - { - initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string); + output.append(1, '\\'); + output.append(1, c); + startPos = currentPos + ESCAPE_LENGTH; + } - escapelessOutput.clear(); - return; - } + } // while +} + +JsonStatusMsg& BasicParse::initError() +{ + error.reset(FB_NEW JsonStatusMsg()); + return *error.get(); +} + +template +void FBJSON::JsonScalarParser::parseQuotedString(TView input, TextPos& current, bool keepEscapeSequence, bool expectEndQuote, SmallString* output) +{ + const bool hasOutput = output != nullptr; + + const TextPos end = input.end(); + constexpr static USHORT chunkSize = 256; - c = input[current++]; + // To check the most common case, using just a simple uint8_t mast is about 10% faster then using an enum + static constexpr auto isSpecial = getSpecialCharacterTable(); + // Interestingly, accessing this std::array is about 20% faster than accessing a C-style ControlType[256] array! + static constexpr auto controlsChars = getControlsTable(); + + while (current < end) + { + std::string_view chunk = input.getStringView(current, chunkSize); + USHORT startPos = 0; - if (c == 'u') + for (USHORT i = 0; i < chunk.length();) + { + UCHAR c = chunk[i++]; + + // Check the most common case without switch! + // 2x time better performance! + if (FB_LIKELY(!isSpecial[c])) + continue; + + // Switch is faster then if-else chain by 10% + const auto charType = controlsChars[c]; + switch (charType) { - // Unicode escape sequence \uXXXX + case ControlType::QUOTE: + { + if (hasOutput) + *output += chunk.substr(startPos, i - startPos - 1); // Exclude the last quote - if (current + UNICODE_HEX_SEQUENCE_LENGTH >= end) + current += i; + return; + } + case ControlType::INVALID: + { + Firebird::string hexStr; + hexStr.printf("%02x", c); + initError() << JsonStatusMsg(isc_jparser_unescaped_character) << hexStr; + current += i - 1; + return; + } + case ControlType::SLASH: + { + // Prefetch chunk for possible unicode + const USHORT lengthWithPrefetchedSize = i + UNICODE_UTF16_SEQUENCE_LENGTH; + if (lengthWithPrefetchedSize >= chunk.length() && current + lengthWithPrefetchedSize < end) { - initError() << JsonStatusMsg(isc_jparser_invalid_sequence) << input.getErrorSubstring(current, end - current); - current = end; + // Part before the escape sequence + const auto appendSize = i - (startPos + 1); - escapelessOutput.clear(); - return; - } + if (hasOutput) + *output += chunk.substr(startPos, appendSize); - // 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; - } - } - } + current += appendSize; + chunk = input.getStringView(current, chunkSize); + i = 1; // symbol after slash + fb_assert(chunk[0] == '\\'); - // 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) + if (current >= end) { - // 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); + initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string); 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 + else if (hasOutput) { - 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; + *output += chunk.substr(startPos, i - (startPos + 1)); } - 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 + c = chunk[i++]; - // We are standing to the last hex digit so start and end with offset of 1 - const TextPos unicodeEndPos = current; + if (c == 'u') + { + // Unicode escape sequence \uXXXX - // 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]); - } + const auto unicode = parseUnicode(chunk.substr(i)); + if (hasOutput) + appendUnicode(unicode.value, chunk.substr(i - 2, unicode.length + 2), *output); - // Two codes, 12 chars - \uXXXX\uXXXX - leadSurrogate = LEAD_SURROGATE_NULL; - } - else // 6 chars - \uXXXX + i += unicode.length; + } + else if (hasOutput) + { + if (keepEscapeSequence) { - for (TextPos i = unicodeEndPos - UNICODE_ESCAPED_SEQUENCE_LENGTH; i < unicodeEndPos; ++i) - { - escapelessOutput.append(1, input[i]); - } + output->append(1, '\\'); + output->append(1, c); } + else + output->append(1, resolveEscapedChar(c)); } - 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); - } + startPos = i; + break; + } //case + } //switch + } // for - // 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; - } + const auto end = chunk.length(); + if (hasOutput) + *output += chunk.substr(startPos, end - startPos); // Exclude the last quote - } // for + current += end; + } // while - if (current >= end) + if (expectEndQuote) { initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string); } @@ -291,7 +253,7 @@ void FBJSON::JsonScalarParser::parseQuotedString(TView& input, TextPos& c template -ParsedNumber FBJSON::JsonScalarParser::parseNumber(TView& input, TextPos& current, bool strictMode) +ParsedNumber FBJSON::JsonScalarParser::parseNumber(TView input, TextPos& current, bool strictMode) { const TextPos start = current; const TextPos end = input.end(); @@ -451,12 +413,6 @@ ParsedNumber FBJSON::JsonScalarParser::parseNumber(TView& input, TextPos& return lex; } -template -JsonStatusMsg& JsonScalarParser::initError() -{ - error.reset(FB_NEW JsonStatusMsg()); - return *error.get(); -} template class FBJSON::JsonScalarParser; -// template class FBJSON::JsonScalarParser; +// template class FBJSON::JsonScalarParser; diff --git a/src/jrd/json/classes/JsonTypes.h b/src/jrd/json/classes/JsonTypes.h index e9cb096d656..48cd55067b7 100644 --- a/src/jrd/json/classes/JsonTypes.h +++ b/src/jrd/json/classes/JsonTypes.h @@ -30,6 +30,14 @@ #include "firebird.h" #include "fb_exception.h" #include "../JsonConsts.h" +#include "../common/utils_proto.h" + +#include "unicode/utf16.h" // U16_IS_TRAIL, U16_GET_SUPPLEMENTARY + +#include +#include +#include + namespace FBJSON { @@ -244,28 +252,284 @@ struct ParsedNumber } }; +struct ParsedUnicode +{ + using ValueType = ULONG; + ValueType value = 0; + UCHAR length = 0; // in bytes +}; -template -class JsonScalarParser +class BasicParse { public: - // JsonStatusMsg allocation is super expansive - Firebird::AutoPtr error; + static inline constexpr USHORT ESCAPE_LENGTH = 2; // \X + static inline constexpr USHORT UNICODE_HEX_SEQUENCE_LENGTH = 4; // XXXXX + static inline constexpr USHORT UNICODE_UTF16_SEQUENCE_LENGTH = UNICODE_HEX_SEQUENCE_LENGTH * 2 + 2; // XXXX\uXXXX + static inline constexpr USHORT UNICODE_ESCAPED_SEQUENCE_LENGTH = 6; // \uXXXX - with the escape + static inline constexpr UCHAR INVALID_HEX = 255; + static inline constexpr UCHAR INVALID_ESCAPE = 0; + + // Keep the size of the enum at 4 bytes, as working with a 4-byte is faster than with a 1-byte + enum class ControlType + { + CHARACTER = 0, + INVALID, + QUOTE, + SLASH + }; + + // A table to get the type of the symbol in a fast way + static constexpr auto getControlsTable() + { + std::array controls{}; + + // 0-32 are non char symbols + for (UCHAR i = 0; i < 32; ++i) + controls[i] = ControlType::INVALID; + + controls['\"'] = ControlType::QUOTE; + controls['\\'] = ControlType::SLASH; + controls[127] = ControlType::INVALID; // del + + return controls; + } + + // A table to check the simple character in a fast way: 0 is a simple char, 1 is a special + static constexpr auto getSpecialCharacterTable() + { + // Accessing a 4-byte value is faster than with a 1-byte step + std::array controls{}; + + // 0-32 are non char symbols + for (UCHAR i = 0; i < 32; ++i) + controls[i] = 1; + + controls['\"'] = 1; + controls['\\'] = 1; + controls[127] = 1; // del + + return controls; + } + + static constexpr auto getHexToDecTable() + { + // Cant use std::optional in constexpr + std::array hexToDec{}; + for (USHORT c = 0; c < hexToDec.size(); ++c) + hexToDec[c] = INVALID_HEX; // Cannot use fill in constexpr + + for (UCHAR c = '0'; c <= '9'; ++c) + hexToDec[c] = c - '0'; + + for (UCHAR c = 'A'; c <= 'F'; ++c) + hexToDec[c] = 10 + c - 'A'; + + for (UCHAR c = 'a'; c <= 'f'; ++c) + hexToDec[c] = 10 + c - 'a'; + + return hexToDec; + } + static constexpr auto getEscapeResolverTable() + { + // Cant use std::optional in constexpr + std::array resolveTable{}; + resolveTable['\\'] = '\\'; + resolveTable['"'] = '"'; + resolveTable['/'] = '/'; + resolveTable['b'] = '\b'; + resolveTable['f'] = '\f'; + resolveTable['n'] = '\n'; + resolveTable['r'] = '\r'; + resolveTable['t'] = '\t'; + return resolveTable; + } + + static constexpr auto getEscapeTable() + { + // Cant use std::optional in constexpr + std::array resolveTable{}; + resolveTable['\\'] = "\\\\"; + resolveTable['"'] = "\\\""; + resolveTable['\b'] = "\\b"; + resolveTable['\f'] = "\\f"; + resolveTable['\n'] = "\\n"; + resolveTable['\r'] = "\\r"; + resolveTable['\t'] = "\\t"; + return resolveTable; + } + + // Instead of std::expected + Firebird::AutoPtr error; bool hasError() const { return error && error->isDirty(); } - void parseQuotedString(TView& view, TextPos& current, SmallString& escapelessOutput); + inline ParsedUnicode::ValueType parseHex(const std::string_view sequence) + { + if (sequence.length() < UNICODE_HEX_SEQUENCE_LENGTH) + { + initError() << JsonStatusMsg(isc_jparser_un_incorrect_code) << sequence; + return 0; + } - // strictMode for unary plus/minus and parse errors - ParsedNumber parseNumber(TView& input, TextPos& current, const bool strictMode); + constexpr static auto hetToDecTable = getHexToDecTable(); + + ParsedUnicode::ValueType hexValue = 0; + char c; + for (USHORT i = 0; i < UNICODE_HEX_SEQUENCE_LENGTH; ++i) + { + c = sequence[i]; + const auto dec = hetToDecTable[c]; + if (dec != INVALID_HEX) + { + hexValue = (hexValue << 4) + dec; + } + else + { + initError() << JsonStatusMsg(isc_jparser_un_incorrect_code) << sequence; + return 0; + } + } + + return hexValue; + } + + ParsedUnicode parseUnicode(const std::string_view sequence) + { + // Possible values + // UTF-8, \uXXXX + // UTF-16, \uXXXX\uXXXX + + // Get a digital code of the handling hex sequence to store it to hexToUnicodeValue + auto hexToUnicodeValue = parseHex(sequence); + if (hasError()) + return {}; + + UCHAR bytesLength = UNICODE_HEX_SEQUENCE_LENGTH; + + // UTF-16, 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 + { + const auto leadSurrogate = hexToUnicodeValue; + + if (sequence.length() < UNICODE_UTF16_SEQUENCE_LENGTH || + sequence[UNICODE_HEX_SEQUENCE_LENGTH] != '\\' || + sequence[UNICODE_HEX_SEQUENCE_LENGTH + 1] != 'u') + { + initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) << sequence; + return {}; + } + + // The second (tail) surrogate is a 16-bit code value in the range U+DC00 to U+DFFF + const auto tailSurrogate = parseHex(sequence.substr(UNICODE_HEX_SEQUENCE_LENGTH + 2)); // skip "\u" + if (hasError()) + return {}; + + if (!U16_IS_TRAIL(tailSurrogate)) + { + initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) << sequence; + return {}; + } + + hexToUnicodeValue = U16_GET_SUPPLEMENTARY(leadSurrogate, tailSurrogate); + bytesLength = UNICODE_UTF16_SEQUENCE_LENGTH; + } + else if (U16_IS_TRAIL(hexToUnicodeValue)) + { + initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) << sequence; + return {}; + } -private: + if (hexToUnicodeValue == 0) + { + initError() << JsonStatusMsg(isc_jparser_un_conv_error); + return {}; + } + + return {hexToUnicodeValue, bytesLength}; + } + + inline char resolveEscapedChar(const char c) + { + static constexpr auto resolver = getEscapeResolverTable(); + + const auto escaped = resolver[c]; + if (FB_UNLIKELY(escaped == INVALID_ESCAPE)) + initError() << JsonStatusMsg(isc_jparser_invalid_sequence) << JsonStatusMsgStrArg(std::string_view(&c, 1)); + + return escaped; + } + + // Static + + static inline ParsedUnicode::ValueType parseHexUnsafe(const std::string_view sequence) + { + constexpr static auto hetToDecTable = getHexToDecTable(); + + ParsedUnicode::ValueType hexValue = 0; + for (USHORT i = 0; i < UNICODE_HEX_SEQUENCE_LENGTH; ++i) + hexValue = (hexValue << 4) + hetToDecTable[sequence[i]]; + + return hexValue; + } + + static ParsedUnicode parseUnicodeUnsafe(const std::string_view sequence) + { + // Possible values + // UTF-8, \uXXXX + // UTF-16, \uXXXX\uXXXX + + // Get a digital code of the handling hex sequence to store it to hexToUnicodeValue + auto hexToUnicodeValue = parseHexUnsafe(sequence); + UCHAR bytesLength = UNICODE_HEX_SEQUENCE_LENGTH; + + // UTF-16, 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 + { + const auto leadSurrogate = hexToUnicodeValue; + + // The second (tail) surrogate is a 16-bit code value in the range U+DC00 to U+DFFF + const auto tailSurrogate = parseHexUnsafe(sequence.substr(UNICODE_HEX_SEQUENCE_LENGTH)); + + hexToUnicodeValue = U16_GET_SUPPLEMENTARY(leadSurrogate, tailSurrogate); + bytesLength = UNICODE_HEX_SEQUENCE_LENGTH * 2; + } + + return {hexToUnicodeValue, bytesLength}; + } + + // Put unicode as char if possible, or append unicodeStr (\uXXXX or \uXXXX\uXXXX) + inline static void appendUnicode(const ParsedUnicode::ValueType unicodeValue, const std::string_view unicodeStr, SmallString& outStr) + { + // C0 Controls and Basic Latin Range: 0000–007F + if (unicodeValue <= 0X007F) + { + outStr.append(1, char(unicodeValue)); + } + else + { + outStr.append(unicodeStr.data(), unicodeStr.length()); + } + } + + static void parseStringUnsafe(const std::string_view input, SmallString& outStr); + +protected: JsonStatusMsg& initError(); }; +template +class JsonScalarParser : public BasicParse +{ +public: + void parseQuotedString(TView view, TextPos& current, bool keepEscapeSequence, bool expectEndQuote, SmallString* output); + + // strictMode for unary plus/minus and parse errors + ParsedNumber parseNumber(TView input, TextPos& current, bool strictMode); +}; + struct StringParseView { @@ -275,6 +539,16 @@ struct StringParseView return base[i]; } + std::string_view getStringView(const TextPos subStart, TextPos subLength) const + { + if (subStart + subLength > base.length()) + { + subLength = base.length() - subStart; + } + + return base.substr(subStart, subLength); + } + SmallString getErrorSubstring(TextPos subStart, TextPos subLength = JSON_MAX_REPORT_SIZE) const { if (subStart + subLength > base.length()) diff --git a/src/jrd/json/path/JPathParser.cpp b/src/jrd/json/path/JPathParser.cpp index 4a5c77d9242..55e5e08a692 100644 --- a/src/jrd/json/path/JPathParser.cpp +++ b/src/jrd/json/path/JPathParser.cpp @@ -381,7 +381,7 @@ int PathParser::yylexAux() std::string_view view(lex.ptr, lex.end - lex.ptr); StringParseView viewp{view}; string* stringPtr = mkTempString(""); - scalarParser.parseQuotedString(viewp, offset, *stringPtr); + scalarParser.parseQuotedString({view}, offset, false, true, stringPtr); lex.ptr += offset; if (scalarParser.hasError()) diff --git a/src/jrd/tests/json/JsonParserTest.cpp b/src/jrd/tests/json/JsonParserTest.cpp new file mode 100644 index 00000000000..dcd9952c51c --- /dev/null +++ b/src/jrd/tests/json/JsonParserTest.cpp @@ -0,0 +1,236 @@ +#include "boost/test/unit_test.hpp" +#include "../TestContext.h" + +#include "../jrd/json/classes/JsonTypes.h" + +#include + +using namespace FBJSON; + + +BOOST_AUTO_TEST_SUITE(JsonSuite) +BOOST_AUTO_TEST_SUITE(ParseTests) + + +BOOST_AUTO_TEST_SUITE(StringParseTests) + +BOOST_AUTO_TEST_CASE(TestSimpleQuotedString) +{ + // Simple, unicode, unicode braek, slash, uni-uni, uni-break-uni + TextPos dummy = 0; + StringParser parser; + parser.parseQuotedString({"Hello \\\" world\""}, dummy, true, true, nullptr); + BOOST_TEST(parser.hasError() == false); + + dummy = 0; + parser.parseQuotedString({"Hello \\\" world"}, dummy, true, false, nullptr); + BOOST_TEST(parser.hasError() == false); + + SmallString output; + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\\" world"}, dummy, true, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello \\\" world"); + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\n world world"}, dummy, true, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello \\n world world"); + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\n world world"}, dummy, false, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello \n world world"); + + dummy = 0; + output.clear(); + + static_assert(char(0x0020) == ' ', "Wrong test"); + parser.parseQuotedString({"Hello \\u0020 world world"}, dummy, false, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello world world"); + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\u0020 world world"}, dummy, true, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello world world"); + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\u0020"}, dummy, true, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello "); + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\ud83d\\ude80 world world"}, dummy, false, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello \\ud83d\\ude80 world world"); + + dummy = 0; + output.clear(); + parser.parseQuotedString({"Hello \\ud83d\\ude80 world\\\" world \\n world \\u0020je\\\""}, dummy, false, false, &output); + BOOST_TEST(parser.hasError() == false); + BOOST_TEST(output.data() == "Hello \\ud83d\\ude80 world\" world \n world je\""); +} + +SmallString removeSlashes(const SmallString& source) +{ + SmallString out = source; + ULONG pos = 0; + while (true) + { + pos = out.find("\\", pos); + if (pos == SmallString::npos) + break; + + auto secondPart = out.substr(pos + 1); + out.resize(pos); + out += secondPart; + } + + return out; +} + +void parse(const SmallString& source, ULONG posToInsert, std::string_view insertion, std::string_view replacement, + SmallString& targetUnescaped, SmallString& targetEscaped, SmallString& outUnescaped, SmallString& outEscaped) +{ + targetEscaped = source; + for (ULONG i = 0; i < insertion.size(); ++i) + targetEscaped[posToInsert + i] = insertion[i]; + + std::string_view testView(targetEscaped.data(), targetEscaped.length()); + + // target = removeSlashes(source); + targetUnescaped = targetEscaped.substr(0, posToInsert); + targetUnescaped += replacement; + targetUnescaped += targetEscaped.substr(posToInsert + insertion.length()); + + + TextPos dummy = 0; + StringParser parser; + + outEscaped.clear(); + parser.parseQuotedString({testView}, dummy, true, false, &outEscaped); + if (parser.hasError()) + outEscaped = ""; + + dummy = 0; + outUnescaped.clear(); + parser.parseQuotedString({testView}, dummy, false, false, &outUnescaped); + if (parser.hasError()) + outUnescaped = ""; +} + +BOOST_AUTO_TEST_CASE(BigStringParse) +{ + ULONG size = 515; + SmallString source; + source.resize(size); + for (ULONG i = 0; i < size; ++i) + { + source[i] = 'a' + (i % ('z' - 'a')); + } + + SmallString targetUnescaped, targetEscaped, outUnescaped, outEscaped; + + parse(source, 256, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + + parse(source, 255, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + + parse(source, 254, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + + parse(source, 253, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + + + // Simple unicode is always converted to ascii + parse(source, 250, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 251, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 252, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 253, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 254, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 255, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 256, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 257, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + + parse(source, 258, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); + BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); + BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); +} + + +BOOST_AUTO_TEST_CASE(TestUnsafeStringParse) +{ + SmallString output; + + StringParser::parseStringUnsafe({"Hello \\\" world"}, output); + BOOST_TEST(output.data() == "Hello \\\" world"); + + StringParser::parseStringUnsafe({"Hello \\n world world"}, output); + BOOST_TEST(output.data() == "Hello \\n world world"); + + static_assert(char(0x0020) == ' ', "Wrong test"); + StringParser::parseStringUnsafe({"Hello \\u0020 world world"}, output); + BOOST_TEST(output.data() == "Hello world world"); + + StringParser::parseStringUnsafe({"Hello \\u0020 world world"}, output); + BOOST_TEST(output.data() == "Hello world world"); + + StringParser::parseStringUnsafe({"Hello \\u0020"}, output); + BOOST_TEST(output.data() == "Hello "); + + StringParser::parseStringUnsafe({"Hello \\ud83d\\ude80 world world"}, output); + BOOST_TEST(output.data() == "Hello \\ud83d\\ude80 world world"); +} + +BOOST_AUTO_TEST_CASE(TestUnsafeStringOverflowBug) +{ + SmallString source = "Hello \\u0020 \\n"; + source[source.length() - 3] = '\0'; // Before \\n + std::string_view view(source.data()); + + //Overflow check + SmallString output; + StringParser::parseStringUnsafe({view}, output); + BOOST_TEST(output.data() == "Hello "); +} +BOOST_AUTO_TEST_SUITE_END() // StringParseTests + +BOOST_AUTO_TEST_SUITE_END() // ParseTests +BOOST_AUTO_TEST_SUITE_END() // JsonSuite From a3eb53fb971ad46278a540d5a8a22a38ed53c757 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Fri, 24 Jul 2026 09:44:08 +0300 Subject: [PATCH 13/20] Remove invalid copt of jpath-parse-conflicts.txt --- builds/posix/make.shared.targets | 1 - 1 file changed, 1 deletion(-) diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index ad8e8962cb7..420bf8ca81b 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -71,7 +71,6 @@ $(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_RO ($(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 2>/dev/null || : ; \ fi From c9e53bc4bfdd150a695a2f9fbce1a17657ba6d01 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Fri, 24 Jul 2026 09:47:04 +0300 Subject: [PATCH 14/20] Add operator== for Firebird::string and string_view --- src/common/classes/fb_string.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/common/classes/fb_string.h b/src/common/classes/fb_string.h index 998f35c5d5d..ec0d165f728 100644 --- a/src/common/classes/fb_string.h +++ b/src/common/classes/fb_string.h @@ -911,6 +911,16 @@ 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);} + + bool operator==(const std::string_view s) const + { + // Special operator for std::string_view. The method `equals` searches for '\0' + // but it may not be present at the location of s.data + s.length(). + // So use a separate implementation for std::string_view + const size_type n = s.length(); + return (length() != n) ? false : (Comparator::compare(c_str(), s, n) == 0); + } + operator std::string_view() const { return std::string_view(stringBuffer, stringLength); } bool getWord(StringType& from, const char* sep) From ad0666bdb7cc7a549b55de6180819f3014f2a396 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Fri, 24 Jul 2026 14:43:21 +0300 Subject: [PATCH 15/20] Fix incorrect argument in fb_string==string_view operator --- src/common/classes/fb_string.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/classes/fb_string.h b/src/common/classes/fb_string.h index ec0d165f728..94c426fe8e8 100644 --- a/src/common/classes/fb_string.h +++ b/src/common/classes/fb_string.h @@ -918,7 +918,7 @@ namespace Firebird // but it may not be present at the location of s.data + s.length(). // So use a separate implementation for std::string_view const size_type n = s.length(); - return (length() != n) ? false : (Comparator::compare(c_str(), s, n) == 0); + return (length() != n) ? false : (Comparator::compare(c_str(), s.data(), n) == 0); } operator std::string_view() const { return std::string_view(stringBuffer, stringLength); } From b1847b4aff138829b5a4202133543830e995c22f Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Wed, 5 Aug 2026 20:52:35 +0300 Subject: [PATCH 16/20] Fix wrong ANY_FIELD declaration --- src/jrd/json/JsonConsts.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jrd/json/JsonConsts.h b/src/jrd/json/JsonConsts.h index 4b07dd33d21..bcd23cc6f33 100644 --- a/src/jrd/json/JsonConsts.h +++ b/src/jrd/json/JsonConsts.h @@ -129,7 +129,7 @@ namespace FBJSON inline constexpr JsonToken DATE = "date"; } - const JsonToken ANY_FIELD("*"); + inline constexpr JsonToken ANY_FIELD("*"); } } From a72c79165243ae1ca236f9063127c685e7b5d54c Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Mon, 10 Aug 2026 09:48:10 +0300 Subject: [PATCH 17/20] Check locale in unit tests --- src/jrd/tests/json/DecimalConvert.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/jrd/tests/json/DecimalConvert.cpp b/src/jrd/tests/json/DecimalConvert.cpp index 027b09daacb..d7b3c434635 100644 --- a/src/jrd/tests/json/DecimalConvert.cpp +++ b/src/jrd/tests/json/DecimalConvert.cpp @@ -15,7 +15,7 @@ class LocaleChangeHolder BOOST_ASSERT(m_prevLocale != nullptr); const char* loc = std::setlocale(LC_NUMERIC, newLocale); - BOOST_ASSERT(loc != nullptr); + m_changed = loc != nullptr; } ~LocaleChangeHolder() @@ -24,8 +24,13 @@ class LocaleChangeHolder BOOST_ASSERT(loc != nullptr); } + inline bool changed() const + { + return m_changed; + } private: char* m_prevLocale; + bool m_changed = false; }; BOOST_AUTO_TEST_SUITE(JsonSuite) @@ -40,6 +45,8 @@ BOOST_AUTO_TEST_CASE(ruUtf8LocaleTest) { SKIP_TEST; // Missing locale LocaleChangeHolder ruEncoding("ru_RU.UTF-8"); + if (!ruEncoding.changed()) + return; FBJSON::NumberConvertBuffer buffer; BOOST_TEST(FBJSON::convertNumberToString(buffer, NUMBER_TO_CONVERT).find('.') != std::string_view::npos); @@ -47,7 +54,9 @@ BOOST_AUTO_TEST_CASE(ruUtf8LocaleTest) BOOST_AUTO_TEST_CASE(enUtf8LocaleTest) { - LocaleChangeHolder ruEncoding("en_US.UTF-8"); + LocaleChangeHolder enEncoding("en_US.UTF-8"); + if (!enEncoding.changed()) + return; FBJSON::NumberConvertBuffer buffer; BOOST_TEST(FBJSON::convertNumberToString(buffer, NUMBER_TO_CONVERT).find('.') != std::string_view::npos); @@ -55,8 +64,9 @@ BOOST_AUTO_TEST_CASE(enUtf8LocaleTest) BOOST_AUTO_TEST_CASE(russianLocaleTest) { - SKIP_TEST // Missing locale LocaleChangeHolder ruEncoding("russian"); + if (!ruEncoding.changed()) + return; FBJSON::NumberConvertBuffer buffer; BOOST_TEST(FBJSON::convertNumberToString(buffer, NUMBER_TO_CONVERT).find('.') != std::string_view::npos); From 309fefaa01a317693d8f6211a455d6c09238aea0 Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Fri, 28 Aug 2026 10:49:54 +0300 Subject: [PATCH 18/20] Place jparse.h to TMP_ROOT --- builds/posix/make.shared.targets | 4 ++-- src/jrd/json/path/JPathParser.h | 4 ++-- src/jrd/json/path/btyacc_json.ske | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets index 420bf8ca81b..ba0d014fbd9 100644 --- a/builds/posix/make.shared.targets +++ b/builds/posix/make.shared.targets @@ -63,7 +63,7 @@ $(OBJ)/.parse-gen-sentinel: $(SRC_ROOT)/dsql/parse.y $(SRC_ROOT)/dsql/btyacc_fb. # 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)/jrd/json/path/JsonPathParser.cpp $(TMP_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 @@ -81,7 +81,7 @@ $(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_RO 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 +$(OBJ)/jrd/json/path/JsonPath.o $(OBJ)/jrd/json/path/JsonPathParser.o $(OBJ)/jrd/RecordSourceNodes.o $(OBJ)/dsql/ExprNodes.o: $(TMP_ROOT)/include/gen/jparse.h # gpre_meta needs a special boot build since there is no database. diff --git a/src/jrd/json/path/JPathParser.h b/src/jrd/json/path/JPathParser.h index e2d177ac9be..5a22a67f91a 100644 --- a/src/jrd/json/path/JPathParser.h +++ b/src/jrd/json/path/JPathParser.h @@ -85,7 +85,7 @@ struct JsonPathContext } // namespace -#include "../gen/jparse.h" +#include "jparse.h" using PassingKeys = Firebird::LeftPooledMap; @@ -220,7 +220,7 @@ class PathParser : public Firebird::PermanentStorage if (type == PathVariable::Type::PASSING) { - m_mainRootCreated = true; // Allow passing instead of root path expression ($.) + m_mainRootCreated = true; // Allow passing instead of root path expression ($.) } } diff --git a/src/jrd/json/path/btyacc_json.ske b/src/jrd/json/path/btyacc_json.ske index b6a9d4c2683..d9a43df842f 100644 --- a/src/jrd/json/path/btyacc_json.ske +++ b/src/jrd/json/path/btyacc_json.ske @@ -21,7 +21,7 @@ #include "../jrd/json/path/JsonPath.h" #include "../jrd/json/path/JPathParser.h" -#include "../gen/jparse.h" +#include "jparse.h" %% tables From b4573d3fd6f989189d125eccc75535ffd4fbc7dd Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Fri, 28 Aug 2026 10:50:19 +0300 Subject: [PATCH 19/20] Fix segfault at engine unit tests cleanup stage --- src/jrd/tests/TestContext.cpp | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/jrd/tests/TestContext.cpp b/src/jrd/tests/TestContext.cpp index 1d3525d12c6..19b952c4d87 100644 --- a/src/jrd/tests/TestContext.cpp +++ b/src/jrd/tests/TestContext.cpp @@ -35,19 +35,30 @@ class CachedAttach return att; } - ~CachedAttach() + void removeDb() { + if (std::filesystem::exists(dbPath)) + std::filesystem::remove(dbPath); + } + + + void shutdown() + { + if (att == nullptr) + return; + att->release(); + att = nullptr; Jrd::TraceManager::getStorage()->shutdown(); removeDb(); } - void removeDb() + ~CachedAttach() { - if (std::filesystem::exists(dbPath)) - std::filesystem::remove(dbPath); + shutdown(); } + Jrd::JProvider prov{nullptr}; Firebird::FbLocalStatus status; @@ -60,6 +71,20 @@ class CachedAttach }; static Firebird::GlobalPtr storage; +// Cleanp of the cached attachment must be done before global FB cleanp or a pool-related segfault will be present +class TestsCleanCallback +{ +public: + TestsCleanCallback() + { } + + ~TestsCleanCallback() + { + storage->shutdown(); + } +}; + +BOOST_TEST_GLOBAL_FIXTURE(TestsCleanCallback); TestContextHolder::TestContextHolder() : m_tdbb(&storage->status, storage->getAttachment(), FB_FUNCTION), From 5705691f2b7c113e0e058ae9cbd948e959d4785e Mon Sep 17 00:00:00 2001 From: Artyom Abakumov Date: Sun, 18 Oct 2026 21:24:47 +0300 Subject: [PATCH 20/20] Fix some issues found during internal review --- builds/posix/Makefile.in | 2 +- builds/win32/msvc15/engine_static.vcxproj | 12 ++-- .../msvc15/engine_static.vcxproj.filters | 26 +++++++- builds/win32/parse.bat | 2 +- doc/sql.extensions/README.json.md | 4 +- src/CMakeLists.txt | 10 ++- src/common/classes/fb_string.h | 2 - src/jrd/json/JsonConsts.h | 6 +- src/jrd/json/JsonRuntime.cpp | 4 +- src/jrd/json/JsonRuntime.h | 6 +- src/jrd/json/JsonUtils.h | 4 +- src/jrd/json/classes/JsonDatetime.cpp | 8 +-- src/jrd/json/classes/JsonDatetime.h | 8 +-- src/jrd/json/classes/JsonScalar.h | 3 +- src/jrd/json/classes/JsonTypes.cpp | 4 +- src/jrd/json/path/JPathParser.cpp | 7 +- src/jrd/json/path/JsonPath.cpp | 4 +- src/jrd/json/path/JsonPath.h | 4 +- src/jrd/json/path/JsonPathParser.y | 27 +++++--- src/jrd/tests/json/JsonParserTest.cpp | 64 +++++++++---------- .../tests/json/classes/JsonDatetimeTest.cpp | 16 ++--- src/jrd/tests/json/classes/JsonScalarTest.cpp | 12 ++-- 22 files changed, 131 insertions(+), 104 deletions(-) diff --git a/builds/posix/Makefile.in b/builds/posix/Makefile.in index d626d889338..72735849065 100644 --- a/builds/posix/Makefile.in +++ b/builds/posix/Makefile.in @@ -1045,7 +1045,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 + $(RM) $(OBJ)/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/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj index 329cacd3acc..d6d74a1ef54 100644 --- a/builds/win32/msvc15/engine_static.vcxproj +++ b/builds/win32/msvc15/engine_static.vcxproj @@ -416,12 +416,12 @@ - - - - - - + + + + + + diff --git a/builds/win32/msvc15/engine_static.vcxproj.filters b/builds/win32/msvc15/engine_static.vcxproj.filters index c6155e4219d..c01cdb75ed6 100644 --- a/builds/win32/msvc15/engine_static.vcxproj.filters +++ b/builds/win32/msvc15/engine_static.vcxproj.filters @@ -1241,7 +1241,7 @@ DSQL - + JSON @@ -1259,5 +1259,29 @@ JSON + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + + + JSON + diff --git a/builds/win32/parse.bat b/builds/win32/parse.bat index 1a1e92c803f..30a4afcbcd9 100644 --- a/builds/win32/parse.bat +++ b/builds/win32/parse.bat @@ -39,7 +39,7 @@ @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 +@sed -n -e "s/.*btyacc: \(.*conflicts.*\)/\1/p" json_y.txt > %FB_ROOT_PATH%\src\jrd\json\path\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 diff --git a/doc/sql.extensions/README.json.md b/doc/sql.extensions/README.json.md index 81918c6f72a..01d055584a1 100644 --- a/doc/sql.extensions/README.json.md +++ b/doc/sql.extensions/README.json.md @@ -4,9 +4,9 @@ Functional to parse, generate, query and store JSON ## JSON Path -The JSON Path is uses to query data from JSON. +The JSON Path is used 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`. +To filter elements, a filter 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: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e1da4a8045..90f226d091b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -284,7 +284,7 @@ add_custom_command( ) add_custom_target(parse_jpath DEPENDS jpath_tab.h jpath_tab.c - SOURCES ${parse_src} jpath_tab.h jpath_tab.c + SOURCES ${jpath_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) @@ -482,6 +482,9 @@ file(GLOB engine_src "jrd/recsrc/*.cpp" "jrd/replication/*.cpp" "jrd/trace/*.cpp" + "jrd/json/*.cpp" + "jrd/json/classes/*.cpp" + "jrd/json/path/*.cpp" "jrd/os/${OS_DIR}/*.cpp" ) set(engine_src ${engine_src} @@ -492,6 +495,7 @@ set(engine_src ${engine_src} ${burp_engine_src} # parse ${GENERATED_DIR}/dsql/parse.cpp + ${GENERATED_DIR}/dsql/JsonPathParser.cpp ) add_src_apple(engine_src jrd/os/posix/unix.cpp @@ -517,8 +521,8 @@ add_epp_suffix(engine_generated_src master) file(GLOB_RECURSE engine_include "dsql/*.h" "jrd/*.h" include/gen/iberror.h) -add_library (engine_common ${engine_src} ${engine_include} ${parse_src}) -add_dependencies_cc (engine_common parse UpdateCloopInterfaces) +add_library (engine_common ${engine_src} ${engine_include} ${parse_src} ${jpath_parse_src}) +add_dependencies_cc (engine_common parse parse_jpath UpdateCloopInterfaces) if (NOT CMAKE_CROSSCOMPILING) diff --git a/src/common/classes/fb_string.h b/src/common/classes/fb_string.h index f7f6f99839e..200a6f0e45f 100644 --- a/src/common/classes/fb_string.h +++ b/src/common/classes/fb_string.h @@ -929,8 +929,6 @@ namespace Firebird return (length() != n) ? false : (Comparator::compare(c_str(), s.data(), n) == 0); } - operator std::string_view() const { return std::string_view(stringBuffer, stringLength); } - bool getWord(StringType& from, const char* sep) { from.alltrim(sep); diff --git a/src/jrd/json/JsonConsts.h b/src/jrd/json/JsonConsts.h index bcd23cc6f33..1cbca13005e 100644 --- a/src/jrd/json/JsonConsts.h +++ b/src/jrd/json/JsonConsts.h @@ -124,8 +124,8 @@ namespace FBJSON inline constexpr JsonToken UNKNOWN = "unknown"; inline constexpr JsonToken TIME_WITH_TZ = "time with time zone"; inline constexpr JsonToken TIME_WITHOUT_TZ = "time without time zone"; - inline constexpr JsonToken TIMESTAMT_WITH_TZ = "timestamp with time zone"; - inline constexpr JsonToken TIMESTAMT_WITHOUT_TZ = "timestamp without time zone"; + inline constexpr JsonToken TIMESTAMP_WITH_TZ = "timestamp with time zone"; + inline constexpr JsonToken TIMESTAMP_WITHOUT_TZ = "timestamp without time zone"; inline constexpr JsonToken DATE = "date"; } @@ -266,7 +266,7 @@ struct JsonStatusMsgWrapper #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_field_name_mismatched "Object field \"@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: " diff --git a/src/jrd/json/JsonRuntime.cpp b/src/jrd/json/JsonRuntime.cpp index e10b88051e0..3a7b7dca12a 100644 --- a/src/jrd/json/JsonRuntime.cpp +++ b/src/jrd/json/JsonRuntime.cpp @@ -23,7 +23,7 @@ * All Rights Reserved. * Contributor(s): ______________________________________. */ - + #include "JsonRuntime.h" #include "JsonConsts.h" @@ -51,7 +51,7 @@ constexpr UCHAR moveOffsetByOperation(JsonExprNode::OffsetCounter& m_offsets, co } // **** -constexpr int isUnaryOperation(const JsonExprOperation op) +constexpr bool isUnaryOperation(const JsonExprOperation op) { return JsonExprOperation::REGEX_FLAGS < op && op < JsonExprOperation::MULTIPLICATION; } diff --git a/src/jrd/json/JsonRuntime.h b/src/jrd/json/JsonRuntime.h index 08bdea3e648..6f5ce831cbe 100644 --- a/src/jrd/json/JsonRuntime.h +++ b/src/jrd/json/JsonRuntime.h @@ -40,7 +40,6 @@ struct ContextVariables; // operations order by its priority enum class JsonExprOperation : UCHAR { - // VALUE, NONE = 0, @@ -154,7 +153,7 @@ class JsonExprNode : public Firebird::PermanentStorage 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) + // The incoming node could be modified and deleted (the function returns false in this case) bool addChild(JsonExprNode* node); // Only for CALCULATION_NODE @@ -239,7 +238,7 @@ class JsonExprNode : public Firebird::PermanentStorage { fb_assert(m_type == JsonExprNode::SCALAR_NODE); - m_data.scalarBlock->value.set(str); + m_data.scalarBlock->value.set(str.ToStringView()); m_type = JsonExprNode::SCALAR_NODE; return this; } @@ -476,7 +475,6 @@ class JsonExprNode : public Firebird::PermanentStorage { } Firebird::AutoPtr variable; //$. or @. or $pass - //JsonVariant* cachedVariable = nullptr; }; struct CalculationBlock : public ChildrenNodes diff --git a/src/jrd/json/JsonUtils.h b/src/jrd/json/JsonUtils.h index 3d506ffb825..c9817d8e892 100644 --- a/src/jrd/json/JsonUtils.h +++ b/src/jrd/json/JsonUtils.h @@ -49,10 +49,10 @@ inline constexpr int NUMBER_PRECISION = 14; template inline std::string_view convertNumberToString(NumberConvertBuffer& buffer, T value) { - // Use to_chars because other function are locale-depended and can insert a ',' instead of a '.' + // Use to_chars because other function are locale-dependent 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 + // Do not use begin/end or front/back because they require some obscure extra casts to char* on Windows char* end = buffer.data() + buffer.size(); if constexpr (std::is_floating_point_v) result = std::to_chars(buffer.data(), end, value, std::chars_format::general, NUMBER_PRECISION); diff --git a/src/jrd/json/classes/JsonDatetime.cpp b/src/jrd/json/classes/JsonDatetime.cpp index 84039baf7e3..5bafe6c18d2 100644 --- a/src/jrd/json/classes/JsonDatetime.cpp +++ b/src/jrd/json/classes/JsonDatetime.cpp @@ -138,7 +138,7 @@ void JsonDatetime::set(Jrd::thread_db* tdbb, const dsc& valueDsc) // 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 + // and other instance stuff. It is more reliable to just use the same function instead of trying to mimic its output Firebird::EXPECT_DATETIME format = DATETIME_SUBTYPE::expect_timestamp_tz; switch (valueDsc.dsc_dtype) @@ -164,7 +164,7 @@ void JsonDatetime::set(Jrd::thread_db* tdbb, const dsc& valueDsc) fb_assert(false); } - m_ts = convertToTimeStamp(m_stringRepresentation, format, m_flags); + m_ts = convertToTimeStamp(m_stringRepresentation.ToStringView(), format, m_flags); } void JsonDatetime::updatePrecision(const USHORT precision) @@ -193,9 +193,9 @@ std::string_view JsonDatetime::getTypeName() const case DATETIME_SUBTYPE::expect_sql_time: return Tokens::TypeName::TIME_WITHOUT_TZ; case DATETIME_SUBTYPE::expect_timestamp_tz: - return Tokens::TypeName::TIMESTAMT_WITH_TZ; + return Tokens::TypeName::TIMESTAMP_WITH_TZ; case DATETIME_SUBTYPE::expect_timestamp: - return Tokens::TypeName::TIMESTAMT_WITHOUT_TZ; + return Tokens::TypeName::TIMESTAMP_WITHOUT_TZ; case DATETIME_SUBTYPE::expect_sql_date: return Tokens::TypeName::DATE; default: diff --git a/src/jrd/json/classes/JsonDatetime.h b/src/jrd/json/classes/JsonDatetime.h index b90104d1449..35f7e88268c 100644 --- a/src/jrd/json/classes/JsonDatetime.h +++ b/src/jrd/json/classes/JsonDatetime.h @@ -36,7 +36,7 @@ #include namespace Firebird { - // Use this for serialization because they have helpful put/get fuctions + // Use this for serialization because they have helpful put/get functions class BlrReader; class BlrWriter; } @@ -70,7 +70,7 @@ class JsonDatetime void set(SmallString&& source, const std::string_view format) { - m_ts = convertToTimeStamp(source, format, m_flags); + m_ts = convertToTimeStamp(source.ToStringView(), format, m_flags); m_stringRepresentation = std::move(source); } @@ -84,7 +84,7 @@ class JsonDatetime void updateFormat(const std::string_view format, Firebird::Callbacks* callback = nullptr) { - m_ts = convertToTimeStamp(m_stringRepresentation, format, m_flags, callback); + m_ts = convertToTimeStamp(m_stringRepresentation.ToStringView(), format, m_flags, callback); } void updatePrecision(const USHORT precision); @@ -96,7 +96,7 @@ class JsonDatetime std::string_view asString() const { - return m_stringRepresentation; + return m_stringRepresentation.ToStringView(); } std::string_view getTypeName() const; diff --git a/src/jrd/json/classes/JsonScalar.h b/src/jrd/json/classes/JsonScalar.h index 2735594be1f..601e1462e42 100644 --- a/src/jrd/json/classes/JsonScalar.h +++ b/src/jrd/json/classes/JsonScalar.h @@ -35,7 +35,7 @@ #include namespace Firebird { - // Use this for serialization because they have helpful put/get fuctions + // Use this for serialization because they have helpful put/get functions class BlrReader; class BlrWriter; } @@ -58,7 +58,6 @@ enum class ValueType : UCHAR JNULL, BOOL, INT, - //JFLOAT, DOUBLE, STRING, DATETIME, // keep it as the last scalar type diff --git a/src/jrd/json/classes/JsonTypes.cpp b/src/jrd/json/classes/JsonTypes.cpp index 682aec3f46d..cd0fde617b0 100644 --- a/src/jrd/json/classes/JsonTypes.cpp +++ b/src/jrd/json/classes/JsonTypes.cpp @@ -33,7 +33,7 @@ #ifdef HAVE_FLOAT_H #include #else -static inline constexpr USHORT DBL_MAX_10_EXP = 308 +static inline constexpr USHORT DBL_MAX_10_EXP = 308; #endif @@ -142,7 +142,7 @@ void FBJSON::JsonScalarParser::parseQuotedString(TView input, TextPos& cu const TextPos end = input.end(); constexpr static USHORT chunkSize = 256; - // To check the most common case, using just a simple uint8_t mast is about 10% faster then using an enum + // To check the most common case, using just a simple uint8_t mask is about 10% faster than using an enum static constexpr auto isSpecial = getSpecialCharacterTable(); // Interestingly, accessing this std::array is about 20% faster than accessing a C-style ControlType[256] array! static constexpr auto controlsChars = getControlsTable(); diff --git a/src/jrd/json/path/JPathParser.cpp b/src/jrd/json/path/JPathParser.cpp index 55e5e08a692..66e204a1513 100644 --- a/src/jrd/json/path/JPathParser.cpp +++ b/src/jrd/json/path/JPathParser.cpp @@ -150,11 +150,6 @@ PathParser::PathParser(MemoryPool& pool, const std::string_view jsonPath) : 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 } @@ -470,7 +465,7 @@ int PathParser::yylexAux() } else if (tok_class & CHR_LETTER) { - // An quoteless filed name or a token or a passing variable + // A quoteless field name or a token or a passing variable // First get only token chars const char* start = lex.ptr - 1; diff --git a/src/jrd/json/path/JsonPath.cpp b/src/jrd/json/path/JsonPath.cpp index 82ac1ba5c0b..ce00dc7bfea 100644 --- a/src/jrd/json/path/JsonPath.cpp +++ b/src/jrd/json/path/JsonPath.cpp @@ -254,8 +254,8 @@ bool PathNode::equalsNoComplex(const JsonLevelNode* const jsonLevel) if (enabledPrev->matchUnwrapPattern()) { // $.field - // [{"filed":42}] - // Expected object but encoutred an array + // [{"field":42}] + // Expected object but encountered an array // Modify path // $.field => $[*].field diff --git a/src/jrd/json/path/JsonPath.h b/src/jrd/json/path/JsonPath.h index 0cf4e57335b..6bee045bea8 100644 --- a/src/jrd/json/path/JsonPath.h +++ b/src/jrd/json/path/JsonPath.h @@ -510,7 +510,7 @@ struct PathNode : Firebird::PermanentStorage inline bool isNextEmpty() const noexcept { - // Two diabled in a row is impossible but check it anyway + // Two disabled in a row is impossible but check it anyway return next == nullptr || (next->isDisabled() && next->isNextEmpty()); } @@ -601,7 +601,7 @@ struct PathNode : Firebird::PermanentStorage UCHAR flags = 0; Firebird::AutoPtr filterNode; - // Filed + // Field SmallString field; // Range diff --git a/src/jrd/json/path/JsonPathParser.y b/src/jrd/json/path/JsonPathParser.y index b23b4b4fc19..8f973fe5348 100644 --- a/src/jrd/json/path/JsonPathParser.y +++ b/src/jrd/json/path/JsonPathParser.y @@ -186,7 +186,7 @@ json_variable // <$ with path> | $passing | | %type path_primary path_primary - : number_scalar // It is easier to place it here and check instead of adding separate rule + : number_scalar // It is easier to place it here and check instead of adding separate rule { $$ = $1; } | QUOTED_STRING { $$ = mkNode()->set(*$1); } @@ -217,6 +217,7 @@ path_accessor } | '(' json_path_arithmetic ')' { + $2->addFlag(JsonExprNode::FLAG_SOLID); $$ = $2; } ; @@ -244,7 +245,10 @@ json_path_expr { if ($2->applyUnaryOp($1)) { - $$ = $2; // Just a scalar + // // Just a scalar, but should return CALCULATION_NODE + auto* node = mkNode(); + node->addChild($2); + $$ = node; } else { @@ -320,8 +324,8 @@ variable_body_opt($context) %type path_body_opt() path_body_opt($context) : /*nothing*/ - | path_filer($context) - | path_filer($context) path_body_accessors($context) + | path_filter($context) + | path_filter($context) path_body_accessors($context) | path_body_accessors($context) ; @@ -339,11 +343,11 @@ accessor_with_opt_filter($context) %type path_filter_opt() path_filter_opt($context) : /* Nothing */ - | path_filer($context) + | path_filter($context) ; -%type path_filer() -path_filer($context) +%type path_filter() +path_filter($context) : filter_expr { if (m_isLaxMode) @@ -470,7 +474,7 @@ single_index($context, $passType) { $$ = -1; } | LAST '-' INTEGER16 { $$ = -1 - $3; } - | LAST '+' INTEGER16 // Allow this obvious out of range value in case of modification (appending element) + | 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 { @@ -654,7 +658,11 @@ unary_predicate predicate : comparison_predicate { $$ = $1; } | filter_function { $$ = $1; } - | '(' boolean_expression ')' { $$ = $2; } + | '(' boolean_expression ')' + { + $2->addFlag(JsonExprNode::FLAG_SOLID); + $$ = $2; + } ; %type comparison_predicate @@ -717,6 +725,7 @@ filter_function auto* root = mkNode(); root->addChild($2); root->addOperation(JsonExprOperation::IS_UNKNOWN); + root->addFlag(JsonExprNode::FLAG_SOLID); $$ = root->finish(); } ; diff --git a/src/jrd/tests/json/JsonParserTest.cpp b/src/jrd/tests/json/JsonParserTest.cpp index dcd9952c51c..0b76dc5840c 100644 --- a/src/jrd/tests/json/JsonParserTest.cpp +++ b/src/jrd/tests/json/JsonParserTest.cpp @@ -140,58 +140,58 @@ BOOST_AUTO_TEST_CASE(BigStringParse) SmallString targetUnescaped, targetEscaped, outUnescaped, outEscaped; parse(source, 256, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetEscaped.data()); parse(source, 255, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetEscaped.data()); parse(source, 254, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetEscaped.data()); parse(source, 253, "\\n", "\n", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetEscaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetEscaped.data()); // Simple unicode is always converted to ascii parse(source, 250, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 251, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 252, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 253, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 254, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 255, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 256, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 257, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); parse(source, 258, "\\u0020", " ", targetUnescaped, targetEscaped, outUnescaped, outEscaped); - BOOST_TEST(std::string_view(outUnescaped) == targetUnescaped.data()); - BOOST_TEST(std::string_view(outEscaped) == targetUnescaped.data()); + BOOST_TEST(outUnescaped.ToStringView() == targetUnescaped.data()); + BOOST_TEST(outEscaped.ToStringView() == targetUnescaped.data()); } @@ -200,23 +200,23 @@ BOOST_AUTO_TEST_CASE(TestUnsafeStringParse) SmallString output; StringParser::parseStringUnsafe({"Hello \\\" world"}, output); - BOOST_TEST(output.data() == "Hello \\\" world"); + BOOST_TEST(output.ToStringView() == "Hello \\\" world"); StringParser::parseStringUnsafe({"Hello \\n world world"}, output); - BOOST_TEST(output.data() == "Hello \\n world world"); + BOOST_TEST(output.ToStringView() == "Hello \\n world world"); static_assert(char(0x0020) == ' ', "Wrong test"); StringParser::parseStringUnsafe({"Hello \\u0020 world world"}, output); - BOOST_TEST(output.data() == "Hello world world"); + BOOST_TEST(output.ToStringView() == "Hello world world"); StringParser::parseStringUnsafe({"Hello \\u0020 world world"}, output); - BOOST_TEST(output.data() == "Hello world world"); + BOOST_TEST(output.ToStringView() == "Hello world world"); StringParser::parseStringUnsafe({"Hello \\u0020"}, output); - BOOST_TEST(output.data() == "Hello "); + BOOST_TEST(output.ToStringView() == "Hello "); StringParser::parseStringUnsafe({"Hello \\ud83d\\ude80 world world"}, output); - BOOST_TEST(output.data() == "Hello \\ud83d\\ude80 world world"); + BOOST_TEST(output.ToStringView() == "Hello \\ud83d\\ude80 world world"); } BOOST_AUTO_TEST_CASE(TestUnsafeStringOverflowBug) diff --git a/src/jrd/tests/json/classes/JsonDatetimeTest.cpp b/src/jrd/tests/json/classes/JsonDatetimeTest.cpp index 15adc4fd1d5..d73b9fc18e1 100644 --- a/src/jrd/tests/json/classes/JsonDatetimeTest.cpp +++ b/src/jrd/tests/json/classes/JsonDatetimeTest.cpp @@ -93,14 +93,14 @@ BOOST_FIXTURE_TEST_CASE(SetFromStringTests, EngineHolder) BOOST_TEST(compareDatetime(test.getTS(), CvtTestUtils::createTimeStampTZ(2018, 1, 1, 1, 23, 45, 120))); - BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMP_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_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMP_WITHOUT_TZ); } @@ -135,21 +135,21 @@ BOOST_FIXTURE_TEST_CASE(SetFromStringWithFormatTests, EngineHolder) BOOST_CHECK(compareDatetime(test.getTS(), CvtTestUtils::createTimeStampTZ(2018, 2, 4, 1, 23, 0, 120))); - BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMP_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); + BOOST_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMP_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_TEST(test.getTypeName() == Tokens::TypeName::TIMESTAMP_WITH_TZ); } @@ -236,7 +236,7 @@ BOOST_FIXTURE_TEST_CASE(DatetimeMethodsTests, EngineHolder) // Move from datetime toString = date.extractString(); - BOOST_TEST((std::string_view)toString == sourceStr); + BOOST_TEST(toString.ToStringView() == sourceStr); // Should be empty after the move toString = date.extractString(); @@ -256,10 +256,10 @@ BOOST_FIXTURE_TEST_CASE(ZeroTimeTests, EngineHolder) 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); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIMESTAMP_WITHOUT_TZ); datetime.set("13.01.2023 00:00 +2:00"sv, ""); - BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIMESTAMT_WITH_TZ); + BOOST_TEST(datetime.getTypeName() == Tokens::TypeName::TIMESTAMP_WITH_TZ); } BOOST_FIXTURE_TEST_CASE(UtcTests, EngineHolder) diff --git a/src/jrd/tests/json/classes/JsonScalarTest.cpp b/src/jrd/tests/json/classes/JsonScalarTest.cpp index de6063a0bdc..c40b4dc831c 100644 --- a/src/jrd/tests/json/classes/JsonScalarTest.cpp +++ b/src/jrd/tests/json/classes/JsonScalarTest.cpp @@ -83,23 +83,23 @@ BOOST_AUTO_TEST_CASE(JsonScalarValues) 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_TEST(scalar.getValue().string->ToStringView() == std::string_view(testValue)); BOOST_CHECK(scalar.isString()); BOOST_CHECK(scalar.isScalar()); } { - std::string_view testValue = "string_view string"; + const 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_TEST(scalar.getValue().string->ToStringView() == testValue); BOOST_CHECK(scalar.isString()); BOOST_CHECK(scalar.isScalar()); // Reset the same value - scalar.set(*scalar.getValue().string); + scalar.set(scalar.getValue().string->ToStringView()); } // DATETIME CHECK @@ -146,7 +146,7 @@ BOOST_AUTO_TEST_CASE(StrMethod) scalar.set("123"); BOOST_TEST(scalar.str() == scalar.getValue().string); - BOOST_TEST(std::string_view(*scalar.str()) == "123"); + BOOST_TEST(scalar.str()->ToStringView() == "123"); } BOOST_AUTO_TEST_CASE(GetStringViewMethod) @@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE(GetStringViewMethod) // 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() == scalar.getValue().string->ToStringView()); BOOST_TEST(scalar.getStringView() == "123"); }