diff --git a/builds/posix/Makefile.in b/builds/posix/Makefile.in
index 9065dae97f8..fe0d00128d8 100644
--- a/builds/posix/Makefile.in
+++ b/builds/posix/Makefile.in
@@ -973,6 +973,7 @@ clean_gpre_gen:
clean_yacc_gen:
$(RM) $(ROOT)/src/dsql/parse.cpp $(ROOT)/src/dsql/dsql.tab.h $(TMP_ROOT)/include/gen/parse.h types.y y.* $(OBJ)/.parse-gen-sentinel
+ $(RM) $(ROOT)/jrd/json/path/JsonPathParser.cpp $(GEN_ROOT)/jtypes.y $(GEN_ROOT)/json_y.y $(TMP_ROOT)/include/gen/jparse.h jtypes.y y.* $(OBJ)/.jparse-gen-sentinel
#___________________________________________________________________________
# Extra platform specific targets
diff --git a/builds/posix/make.shared.targets b/builds/posix/make.shared.targets
index 47c58cb3612..f997f75d9e8 100644
--- a/builds/posix/make.shared.targets
+++ b/builds/posix/make.shared.targets
@@ -61,6 +61,30 @@ $(OBJ)/.parse-gen-sentinel: $(SRC_ROOT)/dsql/parse.y $(SRC_ROOT)/dsql/btyacc_fb.
touch $@
+# This rule creates JsonPathParser.cpp from JsonPathParser.y
+
+$(OBJ)/jrd/json/path/JsonPathParser.cpp $(SRC_ROOT)/include/gen/jparse.h: $(OBJ)/.jparse-gen-sentinel ;
+
+$(OBJ)/.jparse-gen-sentinel: $(SRC_ROOT)/jrd/json/path/JsonPathParser.y $(SRC_ROOT)/jrd/json/path/btyacc_json.ske
+ sed -n '/%type .*/p' < $< > $(GEN_ROOT)/jtypes.y
+ sed 's/%type .*//' < $< > $(GEN_ROOT)/json_y.y
+ ($(BTYACC) -l -d -b jpath -S $(SRC_ROOT)/jrd/json/path/btyacc_json.ske $(GEN_ROOT)/json_y.y; echo $$? > $(GEN_ROOT)/json_y.status) 2>&1 | tee $(GEN_ROOT)/json_y.txt
+ (exit `cat $(GEN_ROOT)/json_y.status`)
+ sed -n -e "s/.*btyacc: \(.*conflicts.*\)/\1/p" $(GEN_ROOT)/json_y.txt > $(TMP_ROOT)/jpath-parse-conflicts.txt
+ cp $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt
+ if [ ! -f $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt ] || ! cmp -s $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt; then \
+ cp $(TMP_ROOT)/jpath-parse-conflicts.txt $(SRC_ROOT)/jrd/json/path/jpath-parse-conflicts.txt || true; \
+ fi
+ sed -i -e 's/#define \([A-Z].*\)/#define TOK_\1/' $(GEN_ROOT)/jpath_tab.h
+ sed -i -e 's/#define TOK_YY\(.*\)/#define YY\1/' $(GEN_ROOT)/jpath_tab.h
+ $(MV) $(GEN_ROOT)/jpath_tab.h $(SRC_ROOT)/include/gen/jparse.h
+ $(MV) $(GEN_ROOT)/jpath_tab.c $(OBJ)/jrd/json/path/JsonPathParser.cpp
+ touch $(OBJ)/jrd/json/path/JsonPathParser.cpp
+
+# Explicit dependence on generated header (jparser)
+$(OBJ)/jrd/json/path/JsonPath.o $(OBJ)/jrd/json/path/JsonPathParser.o $(OBJ)/jrd/RecordSourceNodes.o $(OBJ)/dsql/ExprNodes.o: $(SRC_ROOT)/include/gen/jparse.h
+
+
# gpre_meta needs a special boot build since there is no database.
$(SRC_ROOT)/gpre/gpre_meta.cpp: $(SRC_ROOT)/gpre/gpre_meta.epp
diff --git a/builds/posix/make.shared.variables b/builds/posix/make.shared.variables
index b8cf3f1b8e0..c2b1455afe5 100644
--- a/builds/posix/make.shared.variables
+++ b/builds/posix/make.shared.variables
@@ -86,13 +86,18 @@ AllObjects += $(Chacha_Objects)
Profiler_Objects:= $(call dirObjects,plugins/profiler)
AllObjects += $(Profiler_Objects)
+JSON_Objects:= $(call dirObjects,jrd/json) \
+ $(call dirObjects,jrd/json/path) \
+ $(call dirObjects,jrd/json/classes)
+
# Engine
Engine_Objects:= $(call dirObjects,jrd) $(call dirObjects,dsql) $(call dirObjects,jrd/extds) \
$(call dirObjects,jrd/optimizer) $(call dirObjects,jrd/recsrc) $(call dirObjects,jrd/replication) \
$(call dirObjects,jrd/sys-packages) $(call dirObjects,jrd/trace) \
- $(call makeObjects,lock,lock.cpp)
+ $(call makeObjects,lock,lock.cpp) $(JSON_Objects)
-Engine_Test_Objects:= $(call dirObjects,jrd/tests) $(call dirObjects,lock/tests)
+Engine_Test_Objects:= $(call dirObjects,jrd/tests) $(call dirObjects,lock/tests) \
+ $(call dirObjects,jrd/tests/json) $(call dirObjects,jrd/tests/json/classes)
AllObjects += $(Engine_Objects) $(Engine_Test_Objects)
diff --git a/builds/win32/msvc15/engine_static.vcxproj b/builds/win32/msvc15/engine_static.vcxproj
index 5b0b3ceef1c..7978923259f 100644
--- a/builds/win32/msvc15/engine_static.vcxproj
+++ b/builds/win32/msvc15/engine_static.vcxproj
@@ -200,6 +200,14 @@
+
+
+
+
+
+
+
+
@@ -402,6 +410,12 @@
+
+
+
+
+
+
@@ -422,6 +436,9 @@
+
+ Document
+
{B32D1B09-8161-451E-8D20-D30F26094EC0}
diff --git a/builds/win32/msvc15/engine_static.vcxproj.filters b/builds/win32/msvc15/engine_static.vcxproj.filters
index 9b99e9ebb8f..53f3935906b 100644
--- a/builds/win32/msvc15/engine_static.vcxproj.filters
+++ b/builds/win32/msvc15/engine_static.vcxproj.filters
@@ -46,6 +46,9 @@
{4bcf3dd7-c9c1-4148-bfeb-d5960bf35faa}
+
+ {49f32cf0-a84a-4f19-8efd-58524e855ce2}
+
@@ -1214,5 +1217,23 @@
DSQL
+
+ JSON
+
+
+ JSON
+
+
+ JSON
+
+
+ JSON
+
+
+ JSON
+
+
+ JSON
+
diff --git a/builds/win32/parse.bat b/builds/win32/parse.bat
index e2ea85fd632..1a1e92c803f 100644
--- a/builds/win32/parse.bat
+++ b/builds/win32/parse.bat
@@ -27,4 +27,27 @@
@del y_tab.c
@del sed*
+
+@echo Generating JsonPathParser.cpp
+
+@sed -n "/%%type .*/p" < %FB_ROOT_PATH%\src\jrd\json\path\JsonPathParser.y > jtypes.y
+@sed "s/%%type .*//" < %FB_ROOT_PATH%\src\jrd\json\path\JsonPathParser.y > json_y.y
+
+%FB_ROOT_PATH%\temp\%FB_OBJ_DIR%\btyacc\btyacc -l -d -b jpath -S %FB_ROOT_PATH%\src\jrd\json\path\btyacc_json.ske json_y.y 2>json_y.txt
+@if errorlevel 1 (type json_y.txt && exit /B 1)
+@type json_y.txt
+
+@sed -i "s/#define \([A-Z].*\)/#define TOK_\1/" jpath_tab.h
+@sed -i "s/#define TOK_YY\(.*\)/#define YY\1/" jpath_tab.h
+@sed -n -e "s/.*btyacc: \(.*conflicts.*\)/\1/p" json_y.txt > %FB_ROOT_PATH%\src\dsql\jpath-parse-conflicts.txt
+
+@copy jpath_tab.h %FB_ROOT_PATH%\src\include\gen\jparse.h > nul
+@copy jpath_tab.c %FB_ROOT_PATH%\src\jrd\json\path\JsonPathParser.cpp > nul
+@del json_y.y
+@del json_y.txt
+@del jtypes.y
+@del jpath_tab.h
+@del jpath_tab.c
+@del sed*
+
:END
diff --git a/doc/sql.extensions/README.json.md b/doc/sql.extensions/README.json.md
new file mode 100644
index 00000000000..81918c6f72a
--- /dev/null
+++ b/doc/sql.extensions/README.json.md
@@ -0,0 +1,199 @@
+# JSON
+
+Functional to parse, generate, query and store JSON
+
+## JSON Path
+
+The JSON Path is uses to query data from JSON.
+For example, the path `$[*].name` with JSON `[{"name":"John", "score":10}, {"name":"Sam", "score":50}]` will produce the sequence `["John","name"]`.
+To filter elements, a filer can be used: `$[*] ? (@.score > 15)`. Child filter is also supported: `$[*] ? (@.score > 15).name`.
+
+The JSON Path has 2 modes: lax (default) and strict. The first one allows to flex path and JSON content matching.
+Missing fields and incorrect array ranges are ignored. The strict one produces an error. The full rules:
+1) Invalid array range: lax - allowed, strict - error
+2) Index of array range: lax - allowed, strict - error
+3) Missing field: lax - allowed, strict - error
+4) Error inside a JSON Path Filter: lax - hides (the), strict - throw error
+5) Unwrapping. lax: for `$.name` with `[{"name":"John"},{"name":"Sam"}]`, the array will be unwrapped. A path will be consider as follow: `$[*].name`. Strict - error
+6) Path unwrapping before methods (except `size()` and `type()`) in lax mode
+7) Path unwrapping before filters in lax mode
+8) Path unwrapping before unary `-` and `+`
+9) Path unwrapping for arithmetic operands`
+10) Wrapping. lax: for `$[*].name` with `{"name":"John"}`, the object will be wrapped. A path will be consider as follow: `$.name`. Strict - error
+
+The JSON PATH is used in JSON_VALUE, JSON_QUERY, JSON_EXISTS and JSON_TABLE
+The full syntax:
+
+```sql
+ ::=
+
+
+ ::= lax | strict
+
+ ::=
+ '$' []
+ | '@' []
+ |
+ | []
+ | '(' ')'
+
+ ::= '$'
+
+ ::=
+ []
+ |
+ |
+ |
+
+ ::= [ ...]
+
+ ::=
+
+ |
+ |
+ |
+ |
+
+ ::=
+ '.'
+ | '.'
+
+ ::=
+ '.' '*'
+
+ ::=
+ '[' ']'
+
+ ::=
+ [ { }... ]
+
+ ::=
+
+ | 'last'
+ | 'last' -
+ | 'last' +
+ | to
+ | '*'
+ |
+
+
+ ::=
+ '+'
+ | '-'
+
+ ::=
+ '+'
+ | '-'
+ | '*'
+ | '/'
+ | '%'
+
+
+ ::=
+ '?' '(' ')'
+
+ ::=
+ type '(' ')'
+ | size '(' ')'
+ | double '(' ')'
+ | ceiling '(' ')'
+ | floor '(' ')'
+ | abs '(' ')'
+ | datetime '(' [ ] ')'
+ | keyvalue '(' ')' []
+ | bigint '(' ')'
+ | boolean '(' ')'
+ | date '(' ')'
+ | integer '(' ')'
+ | number '(' ')'
+ | string '(' ')'
+ | time '(' ')'
+ | time_tz '('')'
+ | timestamp '('')'
+ | timestamp_tz '('')'
+
+ ::=
+
+
+
+ ::=
+
+ |
+
+ ::=
+
+ | '(' ')'
+
+ ::=
+ exists '(' ')'
+
+
+ ::=
+
+ |
+ |
+ |
+ |
+
+ ::=
+
+
+ ::=
+ '=='
+ | '!='
+ | '<>'
+ | '<'
+ | '>'
+ | '<='
+ | '>='
+
+ ::=
+ like_regex [ flag ]
+
+ ::=
+ [. ]
+ |
+
+ ::=
+
+ ::=
+
+
+ ::=
+ starts with
+
+ ::=
+ [. ]
+ |
+
+ ::=
+ '(' ')' is unknown
+
+ ::=
+
+ | '!'
+
+
+ ::=
+
+ | '||'
+
+ ::=
+
+ | '&&'
+```
+
+### Nodes
+JSON Path should be a compile time constant
+
+### Examples:
+```sql
+SELECT JSON_VALUE('[1,2,3]', '$[2]') FROM RDB$DATABASE;
+SELECT JSON_QUERY('[1,2,3]', '$ ? (@ > 1)' WITH WRAPPER) FROM RDB$DATABASE;
+SELECT JSON_QUERY('[1,2,3]', 'strict $[*] ? (@ > 1)' WITH WRAPPER) FROM RDB$DATABASE;
+SELECT JSON_QUERY('[1,-2,3]', 'strict $[*].abs()' WITH WRAPPER) FROM RDB$DATABASE;
+
+SELECT JSON_QUERY('[{"name":"John", "score":10}, {"name":"Sam", "score":50}]', '$ ? (@.score > 15).name' WITH WRAPPER) FROM RDB$DATABASE;
+SELECT JSON_QUERY('{"items":[{"name":"John"}, {"name":"Sam", "score":50}]}', '$.items ? (exists(@.score) && @.score < 15).name' WITH WRAPPER) FROM RDB$DATABASE;
+
+```
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 13059dc5ebe..8c7a993d774 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -260,6 +260,34 @@ project_group(parse "Boot/Custom build steps")
set_source_files_properties(dsql/parse.cpp include/gen/parse.h PROPERTIES GENERATED TRUE)
+set(jpath_parse_src
+ ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/JsonPathParser.y
+ ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/btyacc_json.ske
+)
+add_custom_command(
+ OUTPUT jpath_tab.h jpath_tab.c
+ DEPENDS
+ btyacc
+ ${jpath_parse_src}
+ COMMAND sed -n "/%type .*/p" ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/JsonPathParser.y > jtypes.y
+ COMMAND sed "s/%type .*//" ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/JsonPathParser.y > json_y.y
+ COMMAND btyacc -l -d -b jpath -S ${CMAKE_CURRENT_SOURCE_DIR}/jrd/json/path/btyacc_json.ske json_y.y
+
+ COMMAND sed -i "s/#define \\([A-Z].*\\)/#define TOK_\\1/g" jpath_tab.h
+ COMMAND sed -i "s/#define TOK_YY\\(.*\\)/#define YY\\1/g" jpath_tab.h
+
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different jpath_tab.h include/gen/jparse.h
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different jpath_tab.c dsql/JsonPathParser.cpp
+ COMMENT "Generating JsonPathParser.cpp, JsonPathParser.h"
+ VERBATIM
+)
+add_custom_target(parse_jpath
+ DEPENDS jpath_tab.h jpath_tab.c
+ SOURCES ${parse_src} jpath_tab.h jpath_tab.c
+)
+project_group(parse_jpath "Boot/Custom build steps")
+set_source_files_properties(dsql/JsonPathParser.cpp include/gen/jparse.h PROPERTIES GENERATED TRUE)
+
########################################
# BUILD_STEP UpdateCloopInterfaces
########################################
diff --git a/src/common/CvtFormat.cpp b/src/common/CvtFormat.cpp
index 25e911b3881..bd355a0e7c5 100644
--- a/src/common/CvtFormat.cpp
+++ b/src/common/CvtFormat.cpp
@@ -1820,6 +1820,60 @@ namespace
return timestampTZ;
}
+
+ constexpr bool patternContainsDate(const Format::Patterns patterns)
+ {
+ return patterns & (Format::Y
+ | Format::YY
+ | Format::YYY
+ | Format::YYYY
+ | Format::YEAR
+ | Format::RR
+ | Format::RRRR
+ | Format::MM
+ | Format::MON
+ | Format::MONTH
+ | Format::RM
+ | Format::DD
+ | Format::J);
+ }
+
+ constexpr bool patternContainsTime(const Format::Patterns patterns)
+ {
+ return patterns & (Format::HH
+ | Format::HH12
+ | Format::HH24
+ | Format::MI
+ | Format::SS
+ | Format::SSSSS
+ | Format::FF1
+ | Format::FF2
+ | Format::FF3
+ | Format::FF4
+ | Format::AM
+ | Format::PM);
+ }
+
+ constexpr bool patternContainsTimezone(const Format::Patterns patterns)
+ {
+ return patterns & (Format::TZH
+ | Format::TZM
+ | Format::TZR);
+ }
+
+ constexpr Firebird::CvtStringContains::TypeFlags getPatternTypeFlags(const Format::Patterns patterns)
+ {
+ Firebird::CvtStringContains::TypeFlags typeFlags = Firebird::CvtStringContains::NONE;
+
+ if (patternContainsDate(patterns))
+ typeFlags |= Firebird::CvtStringContains::DATE;
+ if (patternContainsTime(patterns))
+ typeFlags |= Firebird::CvtStringContains::TIME;
+ if (patternContainsTimezone(patterns))
+ typeFlags |= Firebird::CvtStringContains::TIMEZONE;
+
+ return typeFlags;
+ }
}
string CVT_format_datetime_to_string(const dsc* desc, const string& format, Callbacks* cb)
@@ -1842,7 +1896,8 @@ string CVT_format_datetime_to_string(const dsc* desc, const string& format, Call
}
ISC_TIMESTAMP_TZ CVT_format_string_to_datetime(const dsc* desc, const Firebird::string& format,
- const EXPECT_DATETIME expectedType, Firebird::Callbacks* cb)
+ const EXPECT_DATETIME expectedType, Firebird::Callbacks* cb,
+ CvtStringContains::TypeFlags* outFormat)
{
if (!DTYPE_IS_TEXT(desc->dsc_dtype))
cb->err(Arg::Gds(isc_invalid_data_type_for_date_format));
@@ -1878,5 +1933,8 @@ ISC_TIMESTAMP_TZ CVT_format_string_to_datetime(const dsc* desc, const Firebird::
validateTimeStamp(timestampTZ.utc_timestamp, cvtData.times, expectedType, desc, cb);
timeStampToUtc(timestampTZ, cb->getSessionTimeZone(), expectedType, cb);
+ if (outFormat)
+ *outFormat = getPatternTypeFlags(formatPatterns);
+
return timestampTZ;
}
diff --git a/src/common/CvtFormat.h b/src/common/CvtFormat.h
index 39e2ab1bfee..96dd89253e5 100644
--- a/src/common/CvtFormat.h
+++ b/src/common/CvtFormat.h
@@ -6,6 +6,7 @@
Firebird::string CVT_format_datetime_to_string(const dsc* desc, const Firebird::string& format, Firebird::Callbacks* cb);
ISC_TIMESTAMP_TZ CVT_format_string_to_datetime(const dsc* desc, const Firebird::string& format,
- const Firebird::EXPECT_DATETIME expectedType, Firebird::Callbacks* cb);
+ const Firebird::EXPECT_DATETIME expectedType, Firebird::Callbacks* cb,
+ Firebird::CvtStringContains::TypeFlags* outFormat = nullptr);
#endif // COMMON_CVT_FORMAT_H
diff --git a/src/common/TimeZoneUtil.cpp b/src/common/TimeZoneUtil.cpp
index 6533976f855..ac2ef01ab8d 100644
--- a/src/common/TimeZoneUtil.cpp
+++ b/src/common/TimeZoneUtil.cpp
@@ -1055,6 +1055,36 @@ ISC_TIMESTAMP_TZ TimeZoneUtil::dateToTimeStampTz(const ISC_DATE& date, Callbacks
return tsTz;
}
+
+int TimeZoneUtil::compareTimeStamps(ISC_TIMESTAMP_TZ lhs, ISC_TIMESTAMP_TZ rhs)
+{
+ if (lhs.time_zone != rhs.time_zone)
+ {
+ localTimeStampToUtc(lhs);
+ localTimeStampToUtc(rhs);
+ }
+
+ return compareUtcTimeStamps(lhs.utc_timestamp, rhs.utc_timestamp);
+}
+
+int TimeZoneUtil::compareUtcTimeStamps(const ISC_TIMESTAMP lhs, const ISC_TIMESTAMP rhs)
+{
+ const NoThrowTimeStamp leftTimestamp(lhs);
+ const NoThrowTimeStamp rightTimestamp(rhs);
+ if (leftTimestamp > rightTimestamp)
+ {
+ return 1;
+ }
+ else if (leftTimestamp < rightTimestamp)
+ {
+ return -1;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
//-------------------------------------
TimeZoneRuleIterator::TimeZoneRuleIterator(USHORT id, const ISC_TIMESTAMP_TZ& aFrom, const ISC_TIMESTAMP_TZ& aTo)
diff --git a/src/common/TimeZoneUtil.h b/src/common/TimeZoneUtil.h
index 6e054356329..bcabd78f5f2 100644
--- a/src/common/TimeZoneUtil.h
+++ b/src/common/TimeZoneUtil.h
@@ -141,6 +141,9 @@ class TimeZoneUtil
static ISC_TIME_TZ timeStampToTimeTz(const ISC_TIMESTAMP& timeStamp, Callbacks* cb);
static ISC_TIMESTAMP_TZ dateToTimeStampTz(const ISC_DATE& date, Callbacks* cb);
+
+ static int compareTimeStamps(ISC_TIMESTAMP_TZ lhs, ISC_TIMESTAMP_TZ rhs);
+ static int compareUtcTimeStamps(const ISC_TIMESTAMP lhs, const ISC_TIMESTAMP rhs);
};
class IcuCalendarWrapper
diff --git a/src/common/classes/BlrReader.h b/src/common/classes/BlrReader.h
index cc654220d28..40f5ba1d276 100644
--- a/src/common/classes/BlrReader.h
+++ b/src/common/classes/BlrReader.h
@@ -201,10 +201,8 @@ class BlrReader
return word;
}
- void getString(string& s)
+ inline void getString(string& s, const unsigned len)
{
- const unsigned len = getByte();
-
if (pos + len >= end)
(Arg::Gds(isc_invalid_blr) << Arg::Num(getOffset())).raise();
@@ -213,6 +211,20 @@ class BlrReader
seekForward(len);
}
+
+ inline void getString(string& s)
+ {
+ const unsigned len = getByte();
+ getString(s, len);
+ }
+
+ inline void getVerbString(string& s)
+ {
+ getByte(); // verb
+ const unsigned len = getWord();
+ getString(s, len);
+ }
+
template
void getMetaName(STR& name)
{
diff --git a/src/common/classes/NoThrowTimeStamp.h b/src/common/classes/NoThrowTimeStamp.h
index 201e9ebaaa6..072fa34c50e 100644
--- a/src/common/classes/NoThrowTimeStamp.h
+++ b/src/common/classes/NoThrowTimeStamp.h
@@ -191,6 +191,28 @@ class NoThrowTimeStamp
return ts;
}
+ inline bool operator>(const NoThrowTimeStamp& rhs) const
+ {
+ const auto rhsValue = rhs.value();
+
+ if (mValue.timestamp_date > rhsValue.timestamp_date)
+ return true;
+ if (mValue.timestamp_date < rhsValue.timestamp_date)
+ return false;
+ return mValue.timestamp_time > rhsValue.timestamp_time;
+ };
+
+ inline bool operator<(const NoThrowTimeStamp& rhs) const
+ {
+ const auto rhsValue = rhs.value();
+
+ if (mValue.timestamp_date < rhsValue.timestamp_date)
+ return true;
+ if (mValue.timestamp_date > rhsValue.timestamp_date)
+ return false;
+ return mValue.timestamp_time < rhsValue.timestamp_time;
+ };
+
private:
ISC_TIMESTAMP mValue;
diff --git a/src/common/classes/fb_string.h b/src/common/classes/fb_string.h
index 657da3fe054..6366a1a6c13 100644
--- a/src/common/classes/fb_string.h
+++ b/src/common/classes/fb_string.h
@@ -793,6 +793,10 @@ namespace Firebird
{
return add(&c, 1);
}
+ StringType& operator=(const std::string_view v)
+ {
+ return assign(v.data(), static_cast(v.length()));
+ }
StringType& operator=(StringType&& rhs)
{
// baseMove do not clear the buffer so do it in this method
@@ -902,6 +906,7 @@ namespace Firebird
bool operator>=(const char_type* str) const {return compare(str) >= 0;}
bool operator> (const char_type* str) const {return compare(str) > 0;}
bool operator!=(const char_type* str) const {return different(str);}
+ operator std::string_view() const { return std::string_view(stringBuffer, stringLength); }
bool getWord(StringType& from, const char* sep)
{
diff --git a/src/common/cvt.cpp b/src/common/cvt.cpp
index da9b6c40d6a..a35256bcd09 100644
--- a/src/common/cvt.cpp
+++ b/src/common/cvt.cpp
@@ -675,7 +675,8 @@ static void integer_to_text(const dsc* from, dsc* to, Callbacks* cb)
void CVT_string_to_datetime(const dsc* desc,
- ISC_TIMESTAMP_TZ* date, bool* timezone_present,
+ ISC_TIMESTAMP_TZ* date,
+ CvtStringContains::TypeFlags* outFormatFlags,
const EXPECT_DATETIME expect_type, bool allow_special, Callbacks* cb)
{
/**************************************
@@ -757,8 +758,8 @@ void CVT_string_to_datetime(const dsc* desc,
memset(components, 0, sizeof(components));
memset(description, 0, sizeof(description));
- if (timezone_present)
- *timezone_present = false;
+ if (outFormatFlags)
+ *outFormatFlags = CvtStringContains::NONE;
// Parse components
// The 7 components are Year, Month, Day, Hours, Minutes, Seconds, Thou
@@ -948,6 +949,9 @@ void CVT_string_to_datetime(const dsc* desc,
continue;
else if (i >= 3 && i <= 5)
{
+ if (outFormatFlags)
+ *outFormatFlags |= CvtStringContains::TIME;
+
if (*p == ':')
{
p++;
@@ -998,8 +1002,8 @@ void CVT_string_to_datetime(const dsc* desc,
{
zone = TimeZoneUtil::parse(p, end - p);
- if (timezone_present)
- *timezone_present = true;
+ if (outFormatFlags)
+ *outFormatFlags |= CvtStringContains::TIMEZONE;
}
}
else
@@ -1076,6 +1080,9 @@ void CVT_string_to_datetime(const dsc* desc,
times.tm_mon = components[position_month];
times.tm_mday = components[position_day];
+ if (outFormatFlags)
+ *outFormatFlags |= CvtStringContains::DATE;
+
// Fetch current date/time
tm times2;
Firebird::TimeStamp::getCurrentTimeStamp().decode(×2);
diff --git a/src/common/cvt.h b/src/common/cvt.h
index 984e6f151d9..1664794e52a 100644
--- a/src/common/cvt.h
+++ b/src/common/cvt.h
@@ -81,6 +81,16 @@ enum EXPECT_DATETIME
expect_sql_time_tz
};
+namespace CvtStringContains
+{
+ typedef UCHAR TypeFlags;
+
+ constexpr TypeFlags NONE = 0;
+ constexpr TypeFlags TIME = 1 << 0;
+ constexpr TypeFlags DATE = 1 << 1;
+ constexpr TypeFlags TIMEZONE = 1 << 2;
+}
+
class Int128;
} // namespace Firebird
@@ -103,7 +113,7 @@ USHORT CVT_get_string_ptr(const dsc*, TTypeId*, UCHAR**, vary*, USHORT, Firebird
USHORT CVT_get_string_ptr_common(const dsc*, TTypeId*, UCHAR**, vary*, USHORT, Firebird::DecimalStatus, Firebird::Callbacks*);
SINT64 CVT_get_int64(const dsc*, SSHORT, Firebird::DecimalStatus, ErrorFunction);
SQUAD CVT_get_quad(const dsc*, SSHORT, Firebird::DecimalStatus, ErrorFunction);
-void CVT_string_to_datetime(const dsc*, ISC_TIMESTAMP_TZ*, bool*, const Firebird::EXPECT_DATETIME,
+void CVT_string_to_datetime(const dsc*, ISC_TIMESTAMP_TZ*, Firebird::CvtStringContains::TypeFlags*, const Firebird::EXPECT_DATETIME,
bool, Firebird::Callbacks*);
const UCHAR* CVT_get_bytes(const dsc*, unsigned&);
diff --git a/src/common/tests/CvtTestUtils.h b/src/common/tests/CvtTestUtils.h
index b7917242419..fa2b57a9853 100644
--- a/src/common/tests/CvtTestUtils.h
+++ b/src/common/tests/CvtTestUtils.h
@@ -29,7 +29,7 @@ static constexpr int sign(T value)
return (value >= T(0)) ? 1 : -1;
}
-static ISC_DATE mockGetLocalDate(int year = 2023)
+inline ISC_DATE mockGetLocalDate(int year = 2023)
{
struct tm time;
memset(&time, 0, sizeof(time));
@@ -41,7 +41,7 @@ static ISC_DATE mockGetLocalDate(int year = 2023)
// Pass 0 to year, month and day to use CurrentTimeStamp for them
-static struct tm initTMStruct(int year, int month, int day)
+inline struct tm initTMStruct(int year, int month, int day)
{
struct tm currentTime;
NoThrowTimeStamp::decode_date(mockGetLocalDate(), ¤tTime);
@@ -58,19 +58,19 @@ static struct tm initTMStruct(int year, int month, int day)
}
// Pass 0 to year, month and day to use CurrentTimeStamp for them
-static ISC_DATE createDate(int year, int month, int day)
+inline ISC_DATE createDate(int year, int month, int day)
{
struct tm times = initTMStruct(year, month, day);
return NoThrowTimeStamp::encode_date(×);
}
-static ISC_TIME createTime(int hours, int minutes, int seconds, int fractions = 0)
+inline ISC_TIME createTime(int hours, int minutes, int seconds, int fractions = 0)
{
return NoThrowTimeStamp::encode_time(hours, minutes, seconds, fractions);
}
// Pass 0 to year, month and day to use CurrentTimeStamp for them
-static ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, int minutes, int seconds, int fractions = 0)
+inline ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, int minutes, int seconds, int fractions = 0)
{
struct tm times = initTMStruct(year, month, day);
times.tm_hour = hours;
@@ -81,7 +81,7 @@ static ISC_TIMESTAMP createTimeStamp(int year, int month, int day, int hours, in
}
// Pass 0 to year, month and day to use CurrentTimeStamp for them
-static ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hours, int minutes, int seconds,
+inline ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hours, int minutes, int seconds,
int offsetInMinutes, int fractions = 0)
{
ISC_TIMESTAMP_TZ timestampTZ;
@@ -94,7 +94,7 @@ static ISC_TIMESTAMP_TZ createTimeStampTZ(int year, int month, int day, int hour
return timestampTZ;
}
-static ISC_TIME_TZ createTimeTZ(int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0)
+inline ISC_TIME_TZ createTimeTZ(int hours, int minutes, int seconds, int offsetInMinutes, int fractions = 0)
{
// Day is 2 because we need to handle 00:00 with negative timezone offset, and anyway date is not used in TIME WITH TIME ZONE
ISC_TIMESTAMP_TZ timestampTz = createTimeStampTZ(1, 1, 2, hours, minutes, seconds, offsetInMinutes, fractions);
diff --git a/src/dsql/make.cpp b/src/dsql/make.cpp
index e30e24359ba..221e886f505 100644
--- a/src/dsql/make.cpp
+++ b/src/dsql/make.cpp
@@ -262,11 +262,15 @@ ValueExprNode* MAKE_constant(const char* str, dsql_constant_type numeric_flag, S
// Now invoke the string_to_date/time/timestamp routines
ISC_TIMESTAMP_TZ ts;
- bool tz;
- CVT_string_to_datetime(&tmp, &ts, &tz, expect1, false, &EngineCallbacks::instance);
+ CvtStringContains::TypeFlags flags;
+ CVT_string_to_datetime(&tmp, &ts, &flags, expect1, false, &EngineCallbacks::instance);
+ bool tz = flags & CvtStringContains::TIMEZONE;
if (!tz && expect1 != expect2)
- CVT_string_to_datetime(&tmp, &ts, &tz, expect2, false, &EngineCallbacks::instance);
+ {
+ CVT_string_to_datetime(&tmp, &ts, &flags, expect2, false, &EngineCallbacks::instance);
+ tz = flags & CvtStringContains::TIMEZONE;
+ }
switch (numeric_flag)
{
diff --git a/src/jrd/json/JsonConsts.h b/src/jrd/json/JsonConsts.h
new file mode 100644
index 00000000000..6b9ad9d6207
--- /dev/null
+++ b/src/jrd/json/JsonConsts.h
@@ -0,0 +1,347 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonConsts.h
+ * DESCRIPTION: JSON constants.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2023 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_CONSTS_H
+#define JSON_CONSTS_H
+
+#include "firebird.h"
+
+#include "../common/classes/fb_string.h"
+#include "../common/StatusArg.h"
+
+#include "../jrd/intl.h"
+#include "../common/TextType.h"
+
+
+static constexpr TTypeId JSON_TTYPE = ttype_utf8;
+static constexpr USHORT JSON_TTYPE_BYTES_BET_CHAR = 4;
+static constexpr USHORT JSON_DEFAULT_SIZE = 6000 * JSON_TTYPE_BYTES_BET_CHAR;
+
+using TextPos = FB_UINT64;
+using TextLength = FB_UINT64;
+
+static constexpr TextPos INVALID_TEXT_POS = MAX_UINT64;
+static constexpr TextLength JSON_MAX_REPORT_SIZE = 50;
+
+
+// Tokens
+namespace FBJSON
+{
+ // usings
+
+ using SmallString = Firebird::string;
+ using JsonToken = std::string_view;
+ using JsonArrayIndex = USHORT;
+
+ enum PlanType : UCHAR // Ordered by priority
+ {
+ DEFAULT = 0,
+ INNER, // one to one from nested
+ OUTER, // one to many with nested
+ UNION, // 2 columns: print full one, then print full the second:,
+ CROSS // 2 columns: print columns at the same moment:
+ };
+
+ enum class CompareResult : int
+ {
+ EQUALS = 0,
+ LESS = -1,
+ MORE = 1
+ };
+
+
+ // Basically the inversion
+ constexpr CompareResult operator*(CompareResult lhs, const bool invert)
+ {
+ if (!invert)
+ return lhs;
+
+ return static_cast(-static_cast(lhs));
+ }
+
+ inline CompareResult castCompareResult(const int r)
+ {
+ if (r < 0)
+ return CompareResult::LESS;
+ else if (r > 0)
+ return CompareResult::MORE;
+ else
+ return CompareResult::EQUALS;
+ }
+
+ namespace Tokens
+ {
+ namespace TypeName
+ {
+ constexpr JsonToken EMPTY = "empty";
+ constexpr JsonToken STRING = "string";
+ constexpr JsonToken NUMBER = "number";
+ constexpr JsonToken BOOLEAN = "boolean";
+ constexpr JsonToken JNULL = "null";
+ constexpr JsonToken ARRAY = "array";
+ constexpr JsonToken OBJECT = "object";
+ constexpr JsonToken DATETIME = "datetime";
+ constexpr JsonToken UNKNOWN = "unknown";
+ constexpr JsonToken TIME_WITH_TZ = "time with time zone";
+ constexpr JsonToken TIME_WITHOUT_TZ = "time without time zone";
+ constexpr JsonToken TIMESTAMT_WITH_TZ = "timestamp with time zone";
+ constexpr JsonToken TIMESTAMT_WITHOUT_TZ = "timestamp without time zone";
+ constexpr JsonToken DATE = "date";
+ }
+
+ const JsonToken ANY_FIELD("*");
+ }
+}
+
+
+// using JsonIscStatus = ISC_STATUS;
+// using JsonStatusVector = Firebird::Arg::StatusVector;
+// using JsonStatusMsg = Firebird::Arg::Gds;
+// using JsonStatusMsgIntArg = Firebird::Arg::Num;
+// using JsonStatusMsgStrArg = Firebird::Arg::Str;
+
+// Temporary classes to avoid constantly moving codes msg/jrd.h;
+// It will be replaced by the real jrd codes
+struct JsonStatusMsgWrapper;
+using JsonIscStatus = const char*;
+using JsonStatusVector = JsonStatusMsgWrapper;
+using JsonStatusMsg = JsonStatusMsgWrapper;
+using JsonStatusMsgIntArg = JsonStatusMsgWrapper;
+using JsonStatusMsgStrArg = JsonStatusMsgWrapper;
+
+struct JsonStatusMsgWrapper
+{
+ Firebird::ObjectsArray impl;
+ mutable Firebird::Arg::StatusVector status;
+
+ JsonStatusMsgWrapper() noexcept : impl(*getDefaultMemoryPool()), status()
+ { }
+
+ JsonStatusMsgWrapper(SINT64 value) : impl(*getDefaultMemoryPool()), status()
+ {
+ Firebird::string str;
+ str.printf("%" SQUADFORMAT, value);
+ impl.push(str);
+ }
+
+
+ JsonStatusMsgWrapper(const Firebird::string& se) : impl(*getDefaultMemoryPool()), status()
+ {
+ impl.push(se);
+ }
+
+ JsonStatusMsgWrapper(const std::string_view sv) : impl(*getDefaultMemoryPool()), status()
+ {
+ impl.push(Firebird::string(sv.data(), static_cast(sv.length())));
+ }
+
+ JsonStatusMsgWrapper(const char* rawStr) : impl(*getDefaultMemoryPool()), status()
+ {
+ Firebird::string str(rawStr);
+ impl.push(str);
+ }
+
+ JsonStatusMsgWrapper(const char* rawStr, ULONG length) : impl(*getDefaultMemoryPool()), status()
+ {
+ Firebird::string str(rawStr, length);
+ impl.push(str);
+ }
+
+ // StatusVector case - append multiple args
+ JsonStatusMsgWrapper& operator<<(const JsonStatusMsgWrapper& arg)
+ {
+ for (FB_SIZE_T i = 0; i < arg.impl.getCount(); i++)
+ {
+ impl.push(arg.impl[i]);
+ }
+
+ return *this;
+ }
+
+ bool isDirty() const
+ {
+ return impl.hasData();
+ }
+
+ const ISC_STATUS* value() const
+ {
+ status.assign(Firebird::Arg::Gds(isc_random) << Firebird::Arg::Str(getMessage()));
+ return status.value();
+ }
+
+ Firebird::string getMessage() const
+ {
+ FB_SIZE_T msgNum = 0;
+ Firebird::string outMsg;
+ // Append non args to the end of the message
+ for (; msgNum < impl.getCount(); msgNum++)
+ {
+ FB_SIZE_T maxUsedMsg = 0;
+ const Firebird::string& mainMsg = impl[msgNum];
+ for (FB_SIZE_T i = 0; i < mainMsg.length(); i++)
+ {
+ const char curChar = mainMsg[i];
+ if (i < mainMsg.length() - 1 && curChar == '@' &&
+ (mainMsg[i + 1] >= '0' && mainMsg[i + 1] <= '9'))
+ {
+ const FB_SIZE_T digit = mainMsg[i + 1] - '0';
+ if (msgNum + digit >= impl.getCount())
+ {
+ fb_assert(false);
+ return "";
+ }
+ else
+ {
+ maxUsedMsg = maxUsedMsg > digit ? maxUsedMsg : digit;
+ outMsg += impl[msgNum + digit];
+ i++;
+ }
+ }
+ else
+ outMsg += curChar;
+ }
+
+ msgNum += maxUsedMsg; // Skip args
+ }
+
+ return outMsg;
+ }
+};
+
+#define isc_jlexer_invalid_syntax "Invalid input syntax"
+#define isc_jlexer_time_precision_syntax "Time precision should be positive"
+#define isc_jlexer_end "Unexpected end of command at position @1"
+#define isc_jlexer_end_with_line "Unexpected end of command at position @1 (line @2)"
+#define isc_jlexer_passing_not_defined "Passing clause has not been set"
+#define isc_jlexer_passing_var_not_defined "Variable '@1' has not been passed. Note that the passing name is case-sensitive"
+#define isc_jpath_common "JSON Path: "
+#define isc_jpath_invalid_token "Invalid input syntax\nToken \"@1\" is invalid"
+#define isc_jpath_problem_place "\n-At position @1\n->@2"
+#define isc_jpath_problem_place_with_line "\n-At position @1 (line @2)\n->@3"
+#define isc_jpath_multiple_roots "Usage of multiple root accessors is not allowed for current JSON function"
+#define isc_jpath_math_is_forbidden "Using methods or arithmetics is not allown in for current function"
+#define isc_jpath_illegal_item_variable "Using @ variable is allowed only in a filter expression"
+#define isc_jpath_missing_root "The root $ expression is missing"
+#define isc_jpath_invalid_range "Range value should be an INTEGRAL value in range [@1 to @2]"
+#define isc_jstrict_common "strict mode: "
+#define isc_jstrict_out_of_range "Range [@1 to @2] is out of array bounds (0..@3)"
+#define isc_jstrict_invalid_range "Range [@1 to @2] is invalid"
+#define isc_jstrict_filed_name_mismatched "Object filed \"@1\" has not been found"
+#define isc_jstrict_path_mismatched "Json Path does not match JSON structure"
+#define isc_jstrict_non_array_size "Cannot calculate size for a non-array token"
+#define isc_jfilter_common "JSON Path Filter: "
+#define isc_jword_string "string"
+#define isc_jword_left "left"
+#define isc_jword_right "right"
+#define isc_jword_unary "unary"
+#define isc_jword_unary_minus "unary minus"
+#define isc_jword_unary_plus "unary plus"
+#define isc_jword_bool_or_unknown "Boolean or Unknown"
+#define isc_jparser_common "JSON parser: invalid input syntax for JSON.\n"
+#define isc_jparser_input "JSON data: @1"
+#define isc_jparser_end_unexpectedly "Expected @1, but encountered the end of the input\n"
+#define isc_jparser_expected_token "Expected @1, but found \"@2\".\n"
+#define isc_jparser_string "string"
+#define isc_jparser_object_end "'}'"
+#define isc_jparser_object_colon "':'"
+#define isc_jparser_array_end "']'"
+#define isc_jparser_value "value"
+#define isc_jparser_end "end of the input"
+#define isc_jparser_unescaped_character "Character with value 0x@1 must be escaped\n"
+#define isc_jparser_un_incorrect_code "The unicode sequence '\\u@1' does not consist of 4 hexadecimal digits\n"
+#define isc_jparser_un_double_high_surrogate "Unicode lead surrogate '\\u@1' must not follow a lead surrogate (U+D800 to U+DBFF)\n"
+#define isc_jparser_un_missing_high_surrogate "The unicode tail surrogate '\\u@1' must follow a lead surrogate (U+D800 to U+DBFF)\n"
+#define isc_jparser_un_conv_error "The Unicode sequence '\\u0000' cannot be converted to text\n"
+#define isc_jparser_invalid_sequence "Escape sequence '\\@1' is invalid\n"
+#define isc_jparser_invalid_token "Token \"@1\" is invalid\n"
+#define isc_jparser_invalid_number_parse "The value \"@1\" cannot be converted to a number\n"
+#define isc_jparser_empty_input "Empty value cannot be converted to a number\n"
+#define isc_jparser_invalid_quoted_string "Invalid quoted string: the closing quote is missing\n"
+#define isc_jparser_number_overflow "Data overflow for number @1\n"
+#define isc_jdyn_common "JSON processor\n"
+#define isc_jdyn_get_double_invalid_type_error "Number expected, but invalid type encountered"
+#define isc_jdyn_get_double_string_type_error "Number expected, but string '@1' encountered"
+#define isc_jdyn_internal_error "internal error (bug)"
+#define isc_jdyn_to_double_error "The statement '@1' cannot be converted to a number"
+#define isc_jdyn_invalid_expression_before_is_unknown "Invalid expression before 'is unknown' predicate"
+#define isc_jdyn_operand_error "@1 operand of operator @2 must be @3"
+#define isc_jdyn_number_operand_error "@1 operand of operator @2 must be a number" // , but a @3 was provided
+#define isc_jdyn_both_not_the_type "Both operations of component @1 must be @2"
+#define isc_jdyn_missing_bool_op "A value without a condition cannot produce a boolean result"
+#define isc_jdyn_missing_value_for_op "Missing value before or after an operand"
+#define isc_jpath_operations_limit "The operations limit has been exceeded (@1, max allowed @2)"
+#define isc_jdyn_json_wrong_type "The value in the JSON representation is expected as a string"
+#define isc_jdyn_invalid_statement "Invalid statement"
+#define isc_jdyn_returns_multiple_values "@1 path expression returns more than one value"
+#define isc_jdyn_returns_non_scalar "@1 path expression returned a non-scalar value"
+#define isc_jdyn_missing_wrapper_scalar "@1 should be used with the ARRAY WRAPPER clause because the result has a scalar value"
+#define isc_jdyn_missing_wrapper_several "@1 should be used with the ARRAY WRAPPER clause because the result consists of several values"
+#define isc_jdyn_incorrect_filter_result "The result of the filter calculation is not a boolean or unknown value"
+#define isc_jdyn_type_unknown_token "type(): An unknown token has been passed"
+#define isc_jdyn_regexflags_not_string "like_regex: The flags clause type is not a string"
+#define isc_jdyn_unknown_regexflag "like_regex: Unknown flag '@1'"
+#define isc_jdyn_flags_illegal_usage "like_regex: Illegal usage of a regex flags clause"
+#define isc_jdyn_token_parsing_error "@1: Error while parsing the input string\n"
+#define isc_jdyn_to_number_error "@1: A non-string or non-number value has been passed"
+#define isc_jdyn_path_empty_result "Cannot operate with empty value/sequence in the current expression"
+#define isc_jdyn_function_empty_result "@1 returned an empty value"
+#define isc_jdyn_key_is_null "A JSON key cannot be NULL"
+#define isc_jdyn_invalid_key_type "Invalid key: Only a character type is allowed"
+#define isc_jdyn_nonunique_keys "There is a non-unique key with the name '@1' at indexes @2 and @3"
+#define isc_jdyn_missing_passing_id "Passing name \"@1\" is not defined"
+#define isc_jmodify_insert_missing_index "Modify error: INSERT mode requires an index range, not an array itself; Use APPEND mode in this case"
+#define isc_jmodify_insert_incompatible "Modify error: INSERT mode is incompatible with the matched JSON token"
+#define isc_jmodify_insert_duplicate "Modify error: attempt to INSERT duplicate key. Use UPDATE mode"
+#define isc_jmodify_mode_vs_object_incompatible "Modify error: Current mode is incompatible with an object"
+#define isc_jmodify_mode_vs_scalar_incompatible "Modify error: Current mode is incompatible with a scalar"
+#define isc_jmodify_missing_item "Item to modify has not been found"
+#define isc_jdyn_not_json "The JSON is not in a proper format"
+#define isc_jdyn_duplicate_fields "The JSON contains duplicate fields"
+#define isc_jdyn_keyvalue_with_scalar "scalar is incomputable with keyvalue()"
+#define isc_jdyn_keyvalue_with_array "array is incomputable with keyvalue()"
+#define isc_jdyn_keyvalue_with_null "null is incomputable with keyvalue()"
+#define isc_jdyn_keyvalue_with_invalid_type "Invalid input type for keyvalue()"
+#define isc_jdyn_size_invalid_type "Invalid input type for size()"
+#define isc_jdyn_datetime_invalid_type "Invalid input type for datetime()"
+#define isc_jdyn_number_limit "Input value is out of JSON number limit"
+#define isc_jdyn_keyvalue_returned_sequence "keyvalue() returned several values an empty sequence"
+#define isc_jdyn_incorrect_path_if_result "The result of the path filter calculation is not a Boolean or Unknown value"
+#define isc_jdyn_datetime_incompatible "Current type is incompatible with the datetime() method"
+#define isc_jdyn_boolean_cast_type "@1: Invalid input type for boolean conversion"
+#define isc_jdyn_boolean_cast_value "@1: The input text cannot be parsed as boolean"
+#define isc_jdyn_json_to_string_prefix "@1: "
+#define isc_jdyn_json_to_string_cast_error "Cannot convert JSON Object or JSON Array to string"
+#define isc_jdyn_to_int_error "@1: Invalid input type for number conversion\n"
+#define isc_jout_enum_with_multiple_columns "A query expression argument in the JSON_ARRAY function must return exactly one column"
+#define isc_jdsc_invalid_subtype "Invalid or not specified JSON subtype"
+#define isc_jdsc_invalid_charset "JSON should be in UTF8 representation"
+#define isc_jdsc_concatenate_unsupported "Concatenation of JSON type is unsupported"
+#define isc_jdsc_non_scalar "The input value is not a scalar"
+#define isc_jdsc_cannot_convert_json_to_dsc "Cannot convert JSON value to a scalar"
+#define isc_jdsc_too_big "The JSON string is too big (the length is @1)"
+
+
+#endif // JSON_CONSTS_H
diff --git a/src/jrd/json/JsonRuntime.cpp b/src/jrd/json/JsonRuntime.cpp
new file mode 100644
index 00000000000..e10b88051e0
--- /dev/null
+++ b/src/jrd/json/JsonRuntime.cpp
@@ -0,0 +1,298 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonRuntime.cpp
+ * DESCRIPTION: Json Calculation logic.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2022 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#include "JsonRuntime.h"
+
+#include "JsonConsts.h"
+
+#include "path/JsonPath.h"
+#include "classes/JsonScalar.h"
+#include "classes/JsonTypes.h"
+
+#include
+
+
+using namespace FBJSON;
+
+// ****
+// Counting sort helpers
+
+constexpr int getOperationIndex(const JsonExprOperation op)
+{
+ return static_cast(op) - JsonExprNode::OFFSET_START;
+}
+
+constexpr UCHAR moveOffsetByOperation(JsonExprNode::OffsetCounter& m_offsets, const JsonExprOperation op)
+{
+ return m_offsets[getOperationIndex(op)]++;
+}
+// ****
+
+constexpr int isUnaryOperation(const JsonExprOperation op)
+{
+ return JsonExprOperation::REGEX_FLAGS < op && op < JsonExprOperation::MULTIPLICATION;
+}
+
+
+JsonExprNode::JsonExprNode(MemoryPool& pool, const Type m_type, const UCHAR flag)
+ : Firebird::PermanentStorage(pool),
+ m_data(),
+ m_type(m_type),
+ m_flags(flag)
+{
+ switch (m_type)
+ {
+ case SCALAR_NODE:
+ m_data.scalarBlock = FB_NEW_POOL(pool) ScalarBlock(pool);
+ break;
+ case VARIABLE_NODE:
+ m_data.varBlock = FB_NEW_POOL(pool) VariableBlock(pool);
+ break;
+ case CALCULATION_NODE:
+ m_data.calculationBlock = FB_NEW_POOL(pool) CalculationBlock(pool);
+ break;
+ case METHOD_NODE:
+ m_data.methodBlock = FB_NEW_POOL(pool) MethodBlock(pool);
+ break;
+ case FILTER_NODE:
+ m_data.filterBlock = FB_NEW_POOL(pool) FilterBlock(pool);
+ break;
+ case COMPOUND_NODE:
+ m_data.childrenBlock = FB_NEW_POOL(pool) ChildrenNodes(pool);
+ break;
+ default:
+ fb_assert(false);
+ }
+}
+
+// Variable node
+JsonExprNode::JsonExprNode(MemoryPool& pool, PathVariable* var) :
+ JsonExprNode(pool, JsonExprNode::VARIABLE_NODE)
+{
+ fb_assert(var);
+ m_data.varBlock = FB_NEW_POOL(pool) VariableBlock(pool);
+ m_data.varBlock->variable.reset(var);
+}
+
+// Method node
+JsonExprNode::JsonExprNode(MemoryPool& pool, const PathMethod method):
+ JsonExprNode(pool, JsonExprNode::METHOD_NODE)
+{
+ m_data.methodBlock = FB_NEW_POOL(pool) MethodBlock(pool);
+
+ m_data.methodBlock->method = method;
+ m_data.methodBlock->children.add(nullptr);
+}
+
+
+JsonExprNode::~JsonExprNode()
+{
+ if (canAccessChildren())
+ {
+ auto& block = *m_data.childrenBlock;
+ for (FB_SIZE_T i = 0; i < block.children.getCount(); i++)
+ delete block.children[i];
+
+ block.children.clear();
+ }
+
+ switch (m_type)
+ {
+ case SCALAR_NODE:
+ delete m_data.scalarBlock;
+ break;
+ case VARIABLE_NODE:
+ delete m_data.varBlock;
+ break;
+ case CALCULATION_NODE:
+ delete m_data.calculationBlock;
+ break;
+ case METHOD_NODE:
+ delete m_data.methodBlock;
+ break;
+ case FILTER_NODE:
+ delete m_data.filterBlock;
+ break;
+ case COMPOUND_NODE:
+ delete m_data.filterBlock;
+ break;
+ default:
+ fb_assert(false);
+ }
+
+ m_data = {};
+}
+
+JsonExprNode* JsonExprNode::addChild(const Type childType, const Flags flags)
+{
+ fb_assert(childType != CALCULATION_NODE || (flags & FLAG_SOLID) != 0); // It is useless to add CALCULATION_NODE with no SOLID flag
+ JsonExprNode* currentLevel = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), childType, flags);
+ return addChild(currentLevel) ? currentLevel : nullptr;
+}
+
+bool JsonExprNode::addChild(JsonExprNode* node)
+{
+ fb_assert(node);
+
+ fb_assert((m_type == CALCULATION_NODE || m_type == COMPOUND_NODE) // Any count of the m_children)
+ || (m_type == METHOD_NODE && m_data.childrenBlock->children.getCount() < 3) // 3 is max(primary, argument, afterpath)
+ || (m_type == FILTER_NODE && m_data.childrenBlock->children.getCount() < 3) // 3 is max
+ || (m_type == VARIABLE_NODE && m_data.childrenBlock->children.getCount() < 1)); // 1 is max
+
+ // A statement is stored in a binary tree
+ // For example:
+ // '1 + 2 * 3' transforms to 'calculation_node(value_node, calculation_node(value_node, value_node))'
+ // It To avoid extra memory usage and simplify the structure we keep only CALCULATION_NODE with brackets
+ if (m_type == CALCULATION_NODE && node->m_type == CALCULATION_NODE && (node->m_flags & FLAG_SOLID) == 0)
+ {
+ for (FB_SIZE_T i = 0; i < node->getChildrenCount(); ++i)
+ {
+ auto* child = node->m_data.calculationBlock->children[i];
+ m_data.childrenBlock->children.add(child);
+ }
+
+ auto& rhsOperations = node->m_data.calculationBlock->operations;
+ for (FB_SIZE_T i = 0; i < rhsOperations.getCount(); ++i)
+ addOperation(rhsOperations[i]);
+
+ // Explicit clear to avoid calling `delete`
+ node->m_data.calculationBlock->children.clear();
+ delete node;
+ return false;
+ }
+ else
+ {
+ m_data.childrenBlock->children.add(node);
+
+ return true;
+ }
+}
+
+void JsonExprNode::addOperation(const JsonExprOperation operation)
+{
+ fb_assert(m_type == CALCULATION_NODE);
+
+ m_data.calculationBlock->operations.add(operation);
+
+ if (m_data.calculationBlock->operations.getCount() > OPERATIONS_LIMIT)
+ {
+ json_syntax_exception::raise(JsonStatusMsg(isc_jpath_operations_limit)<<
+ JsonStatusMsgIntArg(OPERATIONS_LIMIT + 1) << JsonStatusMsgIntArg(OPERATIONS_LIMIT));
+ }
+
+ moveOffsetByOperation(m_data.calculationBlock->offsets, operation);
+}
+
+void JsonExprNode::makeHeadNode(const bool unwrap)
+{
+ fb_assert(canAccessChildren());
+
+ // Variable
+ PathVariable* var = FB_NEW_POOL(getPool()) PathVariable(getPool());
+ var->type = PathVariable::Type::HEAD;
+ var->path = FB_NEW_POOL(getPool()) JsonPath(getPool());
+
+ // Node
+ JsonExprNode* headNode = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), var);
+
+ // Unwrap
+ if (unwrap)
+ {
+ // Only for argument
+ headNode->unwrap();
+ }
+
+ // Add head
+ auto& children = m_data.childrenBlock->children;
+ if (children.getCount() > 0 && children[0] == nullptr)
+ {
+ fb_assert(testType(METHOD_NODE));
+ setHead(headNode);
+ }
+ else
+ addChild(headNode);
+}
+
+
+JsonExprNode* JsonExprNode::finish()
+{
+ fb_assert(m_type == CALCULATION_NODE);
+ if (canBeOmitted()) // Possible useless
+ {
+ auto& children = m_data.calculationBlock->children;
+ auto* node = children.front();
+ children.clear();
+ delete this;
+
+ return node;
+ }
+
+ // Counting sort
+ auto& offsets = m_data.calculationBlock->offsets;
+ int lastValue = offsets[0];
+ for (USHORT i = 1; i < offsets.size(); ++i)
+ {
+ const UCHAR temp = offsets[i];
+ offsets[i] += offsets[i - 1];
+ offsets[i - 1] -= lastValue;
+ lastValue = temp;
+ }
+ fb_assert(offsets.back() == m_data.calculationBlock->operations.getCount());
+ offsets.back() -= lastValue;
+
+ return this;
+}
+
+const PathVariable* JsonExprNode::getVariable() const
+{
+ if (m_type == VARIABLE_NODE)
+ return m_data.varBlock->variable.get();
+ else
+ return nullptr;
+}
+
+void JsonExprNode::unwrap()
+{
+ if (hasTail())
+ {
+ // Recursive call!
+ m_data.calculationBlock->children.back()->unwrap();
+ return;
+ }
+
+ // To unwrap a path, we need a variable node with data
+ if (!isVariable())
+ return;
+
+ auto* variable = m_data.varBlock->variable.get();
+ if (variable == nullptr)
+ return;
+
+ if (variable->path == nullptr)
+ variable->path.reset(FB_NEW_POOL(getPool()) JsonPath(getPool()));
+
+ variable->path->unwrap();
+}
diff --git a/src/jrd/json/JsonRuntime.h b/src/jrd/json/JsonRuntime.h
new file mode 100644
index 00000000000..31a8cfddf37
--- /dev/null
+++ b/src/jrd/json/JsonRuntime.h
@@ -0,0 +1,528 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonRuntime.h
+ * DESCRIPTION: JSON calculation logic.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2022 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_RUNTIME_H
+#define JSON_RUNTIME_H
+
+#include "firebird.h"
+#include "JsonConsts.h"
+#include "classes/JsonScalar.h"
+#include "classes/JsonTypes.h"
+
+namespace FBJSON {
+
+struct ContextVariables;
+
+// operations order by its priority
+enum class JsonExprOperation : UCHAR
+{
+ // VALUE,
+ NONE = 0,
+
+
+ // Functions
+ STARTS_WITH,
+ LIKE_REGEX,
+ REGEX_FLAGS,
+
+ // Unary operations
+ IS_UNKNOWN,
+ EXISTS,
+
+ // Unary
+ UNARY_PLUS,
+ UNARY_MINUS,
+
+ // Logical negation
+ NOT,
+
+ // Arithmetic operations
+ MULTIPLICATION,
+ DIVISION,
+ MODULO,
+
+ ADDITION,
+ SUBTRACTION,
+
+ // Logical operations
+ MORE,
+ MORE_OE,
+ LESS,
+ LESS_OE,
+ EQUALS,
+ NOT_EQUALS,
+
+ // Boolean operations
+ AND,
+ OR,
+
+ // Keep it last
+ LAST
+};
+
+class PassingList;
+struct PathVariable;
+
+
+// * * * *
+// A main class to represent and execute complex expressions with arithmetics, comparators and methods
+// All logic is set within a single class. It is not worth of splitting it into several classes
+// because it only leads to overcomplicated a pretty simple and limited functional
+
+// Some node has a special storage strategy for children nodes:
+// is a primary value (usually scalar or a variable)
+// are used for the current node
+// is a next execution node
+
+// * * * *
+class JsonExprNode : public Firebird::PermanentStorage
+{
+public:
+ static constexpr USHORT OFFSET_START = static_cast(JsonExprOperation::NONE) + 1;
+ static constexpr USHORT OFFSET_SIZE = static_cast(JsonExprOperation::LAST) - OFFSET_START;
+ static constexpr USHORT OPERATIONS_LIMIT = 128;
+
+ using OffsetCounter = std::array;
+
+ enum Type : UCHAR
+ {
+ SCALAR_NODE, // Just a value
+ VARIABLE_NODE, // $. @. $passingVariable
+ CALCULATION_NODE, // An expression
+ METHOD_NODE, // Method with args
+ FILTER_NODE, // Filter: $ ? (@ > 2)
+ COMPOUND_NODE // A set of 2 or more nodes
+ };
+
+ enum Flags : UCHAR
+ {
+ FLAG_NONE = 0,
+ FLAG_METHOD_ARGUMENT = 1,
+ FLAG_SOLID = 2, // in brackets
+ };
+
+ template
+ requires(T != JsonExprNode::Type::METHOD_NODE && T != JsonExprNode::Type::VARIABLE_NODE)
+ static JsonExprNode* make(MemoryPool& pool, const UCHAR flag = FLAG_NONE)
+ {
+ return FB_NEW_POOL(pool) JsonExprNode(pool, T);
+ }
+
+ // It is better to use `make` function to create a new node
+ explicit JsonExprNode(MemoryPool& pool, const Type type = CALCULATION_NODE, const UCHAR flag = FLAG_NONE);
+
+ // Variable node
+ JsonExprNode(MemoryPool& pool, PathVariable* var);
+
+ // Method node
+ JsonExprNode(MemoryPool& pool, const PathMethod method);
+
+ JsonExprNode(JsonExprNode&&) noexcept = delete;
+ JsonExprNode(const JsonExprNode&) = delete;
+
+ JsonExprNode& operator=(JsonExprNode&&) noexcept = delete;
+ JsonExprNode& operator=(const JsonExprNode&) = delete;
+
+ ~JsonExprNode();
+
+public:
+ void passPassing(const PassingList* passing);
+
+public:// Children routines
+ JsonExprNode* addChild(const Type childType, const Flags flags = FLAG_NONE);
+
+ // The incoming node cloud be modifed and deleted (the function returns false in this case)
+ bool addChild(JsonExprNode* node);
+
+ // Only for CALCULATION_NODE
+ void addOperation(const JsonExprOperation operation);
+
+ // Value methods
+ inline JsonExprNode* addVariable(PathVariable* main)
+ {
+ JsonExprNode* currentLevel = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), main);
+ return addChild(currentLevel) ? currentLevel : nullptr;
+ }
+
+ // Make a new node and insert it as first element
+ void makeHeadNode(const bool unwrap);
+
+ inline void setHead(JsonExprNode* base)
+ {
+ fb_assert(canAccessChildren());
+ fb_assert(m_data.childrenBlock->children[0] == nullptr || m_data.childrenBlock->children[0]->m_type == VARIABLE_NODE);
+ m_data.childrenBlock->children[0] = base;
+ }
+
+ // Call it after all addChild, when all operators and values are added
+ JsonExprNode* finish();
+
+public: // Set content methods
+ inline JsonExprNode* set(const int value) noexcept
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.set(value);
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+ inline JsonExprNode* set(const SINT64 value) noexcept
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.set(value);
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+ inline JsonExprNode* set(const double value) noexcept
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.set(value);
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+ inline JsonExprNode* set(const bool value) noexcept
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.set(value);
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+ inline JsonExprNode* set(const char* value) noexcept
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.set(value);
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+ inline JsonExprNode* setNull() noexcept
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.setToNull();
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+ inline JsonExprNode* set(const SmallString& str)
+ {
+ fb_assert(m_type == JsonExprNode::SCALAR_NODE);
+
+ m_data.scalarBlock->value.set(str);
+ m_type = JsonExprNode::SCALAR_NODE;
+ return this;
+ }
+
+public: // Getters
+ inline JsonExprNode* getFirstChild() const
+ {
+ fb_assert(canAccessChildren());
+ fb_assert(m_data.childrenBlock->children.getCount() > 0);
+ return m_data.childrenBlock->children[0];
+ }
+
+ inline JsonExprNode* getSecondChild() const
+ {
+ fb_assert(canAccessChildren());
+ fb_assert(m_data.childrenBlock->children.getCount() > 1);
+ return m_data.childrenBlock->children[1];
+ }
+
+ inline JsonExprNode* getLastChild() const
+ {
+ fb_assert(canAccessChildren());
+ fb_assert(m_data.childrenBlock->children.getCount() > 0);
+ return m_data.childrenBlock->children[m_data.childrenBlock->children.getCount() - 1];
+ }
+
+ inline FB_SIZE_T getChildrenCount() const noexcept
+ {
+ if (!canAccessChildren())
+ return 0;
+
+ return m_data.childrenBlock->children.getCount();
+ }
+
+ inline void clearChildren() noexcept
+ {
+ fb_assert(canAccessChildren());
+ m_data.childrenBlock->children.clear();
+ }
+
+public:// Checkers
+ inline bool isRootEmpty() const
+ {
+ switch (m_type)
+ {
+ case JsonExprNode::CALCULATION_NODE:
+ {
+ auto& children = m_data.calculationBlock->children;
+ auto& operations = m_data.calculationBlock->operations;
+ return (children.isEmpty() || children.getCount() == 1 && children[0]->isRootEmpty()) && operations.isEmpty();
+ }
+ case JsonExprNode::COMPOUND_NODE:
+ case JsonExprNode::FILTER_NODE:
+ return m_data.childrenBlock->children.isEmpty();
+ default:
+ return false;
+ }
+ }
+
+ inline bool isVariable() const
+ {
+ return m_type == JsonExprNode::VARIABLE_NODE;
+ }
+
+ inline bool testType(const JsonExprNode::Type type) const
+ {
+ return this->m_type == type;
+ }
+
+ inline bool canHasTail() const
+ {
+ switch (m_type)
+ {
+ case JsonExprNode::METHOD_NODE:
+ case JsonExprNode::VARIABLE_NODE:
+ case JsonExprNode::FILTER_NODE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ inline bool hasTail() const
+ {
+ switch (m_type)
+ {
+ case METHOD_NODE:
+ return m_data.childrenBlock->children.getCount() >= 2 && (m_data.childrenBlock->children.back()->m_flags & FLAG_METHOD_ARGUMENT) == 0;
+ case VARIABLE_NODE:
+ return m_data.childrenBlock->children.getCount() == 1;
+ case FILTER_NODE:
+ return m_data.childrenBlock->children.getCount() == 3;
+ default:
+ return false;
+ }
+ }
+
+ inline JsonExprNode* getTailNode() const
+ {
+ if (hasTail())
+ return m_data.childrenBlock->children.back(); // Always the last
+ else
+ return nullptr;
+ }
+
+ inline PathMethod getPathMethod() const noexcept
+ {
+ if (!testType(METHOD_NODE))
+ return PathMethod::NONE;
+
+ return m_data.methodBlock->method;
+ }
+
+ const PathVariable* getVariable() const;
+
+ inline bool applyUnaryOp(const JsonExprOperation op) noexcept
+ {
+ fb_assert(op == JsonExprOperation::UNARY_MINUS || op == JsonExprOperation::UNARY_PLUS);
+
+ if (!testType(SCALAR_NODE))
+ return false;
+
+ if (op == JsonExprOperation::UNARY_PLUS)
+ return true;
+
+ if (m_data.scalarBlock->value.getType() == ValueType::INT)
+ {
+ m_data.scalarBlock->value.getValue().integer = -m_data.scalarBlock->value.getValue().integer;
+ return true;
+ }
+ else
+ {
+ m_data.scalarBlock->value.getValue().doubleValue = -m_data.scalarBlock->value.getValue().doubleValue;
+ return true;
+ }
+ }
+
+ std::optional getRangeNumber() const
+ {
+ if (!testType(SCALAR_NODE))
+ return std::nullopt;
+
+ static constexpr RangeSize rangeMin = std::numeric_limits::min();
+ static constexpr RangeSize rangeMax = std::numeric_limits::max();
+
+ auto invalidRangeOrTypeError = []()
+ {
+ json_fatal_exception::raise(JsonStatusMsgWrapper(isc_jpath_common) <<
+ JsonStatusMsgWrapper(isc_jpath_invalid_range) << rangeMin << rangeMax);
+ };
+
+ if (!m_data.scalarBlock->value.isNumber())
+ invalidRangeOrTypeError();
+
+ if (m_data.scalarBlock->value.getType() != ValueType::INT)
+ invalidRangeOrTypeError();
+
+ const auto value = m_data.scalarBlock->value.getValue().integer;
+ if (value < rangeMin || value > rangeMax)
+ invalidRangeOrTypeError();
+
+
+ return static_cast(value);
+ }
+
+ // A calculation node with only one child is useless in term of this node
+ // So it is safe to omit such node and keep only the child
+ bool canBeOmitted() const
+ {
+ return m_type == CALCULATION_NODE && getChildrenCount() == 1 && !m_data.calculationBlock->operations.hasData();
+ }
+
+public: // Helpers
+ // Unwrap path in a variable node
+ void unwrap();
+
+ constexpr void addFlag(const Flags flag)
+ {
+ m_flags |= UCHAR(flag);
+ }
+
+public: // Executions
+ enum class FilterResult
+ {
+ // Add the 'R' prefix to avoid conflicts with macros
+ RTRUE,
+ RFALSE,
+ RUNKNOWN
+ };
+ //static FilterResult passFilter(const ContextVariables& context, const JsonExprNode* filter);
+
+ // Executers
+ //JsonVariant execute(const ContextVariables& context) const;
+
+private:
+ inline bool canAccessChildren() const noexcept
+ {
+ // Basically all except SCALAR_NODE
+ return m_type == CALCULATION_NODE || m_type == FILTER_NODE || m_type == METHOD_NODE ||
+ m_type == VARIABLE_NODE || m_type == COMPOUND_NODE;
+ }
+
+ //JsonVariant executeVariableNode(const ContextVariables& context) const;
+ //JsonVariant executeCalculationNode(const ContextVariables& context) const;
+ //JsonVariant executeMethodNode(const ContextVariables& context) const;
+ //JsonVariant executeFilterNode(const ContextVariables& context) const;
+
+private:
+ // Scalar node
+
+ struct ScalarBlock
+ {
+ JsonScalar value;
+
+ ScalarBlock(MemoryPool& pool) :
+ value(pool)
+ { }
+ };
+
+
+ struct ChildrenNodes
+ {
+ Firebird::HalfStaticArray children;
+
+ ChildrenNodes(MemoryPool& pool) :
+ children(pool)
+ { }
+ };
+
+ struct VariableBlock : public ChildrenNodes
+ {
+ VariableBlock(MemoryPool& pool) :
+ ChildrenNodes(pool)
+ { }
+
+ Firebird::AutoPtr variable; //$. or @. or $pass
+ //JsonVariant* cachedVariable = nullptr;
+ };
+
+ struct CalculationBlock : public ChildrenNodes
+ {
+ CalculationBlock(MemoryPool& pool) :
+ ChildrenNodes(pool),
+ operations(pool)
+ { }
+
+ Firebird::HalfStaticArray operations;
+ // Counter of each operation for correct execution order
+ OffsetCounter offsets = {};
+ };
+
+ // Variable node
+ struct MethodBlock : public ChildrenNodes
+ {
+ // Method node
+ PathMethod method = PathMethod::NONE;
+ MethodBlock(MemoryPool& pool) :
+ ChildrenNodes(pool)
+ { }
+ };
+
+ struct FilterBlock : public ChildrenNodes
+ {
+ FilterBlock(MemoryPool& pool) :
+ ChildrenNodes(pool)
+ { }
+ };
+
+ union NodeData
+ {
+ ScalarBlock* scalarBlock;
+ VariableBlock* varBlock;
+ ChildrenNodes* childrenBlock;
+ CalculationBlock* calculationBlock;
+ MethodBlock* methodBlock;
+ FilterBlock* filterBlock;
+ } m_data{};
+
+ // Common data
+ Type m_type;
+ UCHAR m_flags{};
+};
+
+}
+
+#endif // JSON_RUNTIME_H
diff --git a/src/jrd/json/JsonUtils.h b/src/jrd/json/JsonUtils.h
new file mode 100644
index 00000000000..054be3401ad
--- /dev/null
+++ b/src/jrd/json/JsonUtils.h
@@ -0,0 +1,110 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonUtils.h
+ * DESCRIPTION: Common functions to work with JSON.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2025 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_UTILS_H
+#define JSON_UTILS_H
+
+#include "firebird.h"
+#include "../../common/StatusArg.h"
+
+#include
+#include // std::to_chars
+#include
+
+#ifdef ANDROID
+#include "../common/utils_proto.h"
+#endif
+
+
+namespace FBJSON
+{
+
+using NumberConvertBuffer = std::array;
+
+inline constexpr int NUMBER_PRECISION = 14;
+
+template
+inline std::string_view convertNumberToString(NumberConvertBuffer& buffer, T value)
+{
+ // Android does not have to_chars
+#ifdef ANDROID
+ ULONG outputLength = 0;
+
+ if constexpr (std::is_floating_point_v)
+ {
+ outputLength = fb_utils::snprintf(buffer.data(), buffer.size(), "%.*g", NUMBER_PRECISION, value);
+
+ // Replace the ',' with '.'
+ for (char* it = buffer.begin(); it != buffer.end(); ++it)
+ {
+ if (*it == ',')
+ {
+ *it = '.';
+ break;
+ }
+ }
+ }
+ else
+ outputLength = fb_utils::snprintf(buffer.data(), buffer.size(), "%" SQUADFORMAT, value);
+
+ return std::string_view(buffer.data(), outputLength);
+#else
+ //! Use to_chars because other function are locale-depended and can insert a ',' instead of a '.'
+ std::to_chars_result result;
+
+ //! Do not use begin/end or front/back because they requeue some obscure extra casts to char* on Windows
+ if constexpr (std::is_floating_point_v)
+ result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value, std::chars_format::general, NUMBER_PRECISION);
+ else
+ result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value);
+
+ if (result.ec != std::errc())
+ {
+ fb_assert(false);
+ Firebird::Arg::Gds(isc_arith_except).raise();
+ }
+
+ return std::string_view(buffer.data(), result.ptr - buffer.data());
+#endif
+}
+
+template
+constexpr USHORT getSuperType(T op1, T op2)
+{
+ return (static_cast(op1) << 8) | static_cast(op2);
+}
+
+enum BoolSuperType : USHORT
+{
+ TRUE_VS_FALSE = getSuperType(true, false),
+ FALSE_VS_TRUE = getSuperType(false, true),
+ TRUE_VS_TRUE = getSuperType(true, true),
+ FALSE_VS_FALSE = getSuperType(false, false)
+};
+
+} // namespace
+
+#endif // !JSON_UTILS_H
diff --git a/src/jrd/json/classes/JsonDatetime.cpp b/src/jrd/json/classes/JsonDatetime.cpp
new file mode 100644
index 00000000000..84039baf7e3
--- /dev/null
+++ b/src/jrd/json/classes/JsonDatetime.cpp
@@ -0,0 +1,338 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonDatetime.cpp
+ * DESCRIPTION: Common types for JSON code.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2024 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+
+#include "JsonDatetime.h"
+#include "../JsonConsts.h"
+
+// Engine stuff for datetime parsing
+#include "../common/cvt.h"
+#include "../common/dsc.h"
+#include "../common/TimeZoneUtil.h"
+#include "../common/CvtFormat.h"
+#include "../common/classes/BlrReader.h"
+#include "../common/classes/BlrWriter.h"
+
+#include "../jrd/cvt_proto.h"
+#include "../jrd/mov_proto.h"
+
+namespace FBJSON{
+
+using DATETIME_SUBTYPE = Firebird::EXPECT_DATETIME;
+
+
+// Use custom throw function because EngineCallbacks::instance make tdbb_status dirty
+class JsonErrorCallback : public Jrd::EngineCallbacks
+{
+public:
+ JsonErrorCallback(MemoryPool&) :
+ Jrd::EngineCallbacks(throwError)
+ { }
+
+ [[noreturn]] static void throwError(const Firebird::Arg::StatusVector& v)
+ {
+ v.raise();
+ }
+};
+Firebird::GlobalPtr jsonCallback;
+
+struct DatetimeUtils
+{
+ ISC_TIMESTAMP_TZ inputUtcTimestamp;
+ ISC_TIMESTAMP localBuffer;
+ ISC_TIME_TZ timeTzBuffer;
+
+ static DATETIME_SUBTYPE getFormat(const Firebird::CvtStringContains::TypeFlags flags)
+ {
+ using namespace std::literals;
+ using namespace Firebird::CvtStringContains;
+
+ switch (flags)
+ {
+ case TIME | TIMEZONE:
+ return DATETIME_SUBTYPE::expect_sql_time_tz;
+ case TIME:
+ return DATETIME_SUBTYPE::expect_sql_time;
+ case DATE | TIMEZONE:
+ case DATE | TIME | TIMEZONE:
+ return DATETIME_SUBTYPE::expect_timestamp_tz;
+ case DATE | TIME:
+ return DATETIME_SUBTYPE::expect_timestamp;
+ case DATE:
+ return DATETIME_SUBTYPE::expect_sql_date;
+ default:
+ fb_assert(false);
+ return DATETIME_SUBTYPE::expect_sql_time_tz;
+ }
+ }
+
+ ISC_TIMESTAMP& getLocal()
+ {
+ return localBuffer = Firebird::TimeZoneUtil::timeStampTzToTimeStamp(inputUtcTimestamp,
+ JRD_get_thread_data()->getAttachment()->att_current_timezone);
+ }
+
+ dsc makeDatetimeDsc(const ISC_TIMESTAMP_TZ utcTimestamp, const JsonDatetime::Flags flags)
+ {
+ inputUtcTimestamp = utcTimestamp;
+
+ dsc timestampDsc;
+
+ switch (getFormat(flags))
+ {
+ case DATETIME_SUBTYPE::expect_sql_time:
+ timestampDsc.makeTime(&getLocal().timestamp_time);
+ break;
+ case DATETIME_SUBTYPE::expect_sql_time_tz:
+ timeTzBuffer.utc_time = utcTimestamp.utc_timestamp.timestamp_time;
+ timeTzBuffer.time_zone = utcTimestamp.time_zone;
+ timestampDsc.makeTimeTz(&timeTzBuffer);
+ break;
+ case DATETIME_SUBTYPE::expect_sql_date:
+ timestampDsc.makeDate(&getLocal().timestamp_date);
+ break;
+ case DATETIME_SUBTYPE::expect_timestamp:
+ timestampDsc.makeTimestamp(&getLocal());
+ break;
+ case DATETIME_SUBTYPE::expect_timestamp_tz:
+ timestampDsc.makeTimestampTz(&inputUtcTimestamp);
+ break;
+ default:
+ fb_assert(false);
+ timestampDsc.makeTimestampTz(&inputUtcTimestamp);
+ break;
+ }
+
+ return timestampDsc;
+ }
+};
+
+
+void JsonDatetime::set(Jrd::thread_db* tdbb, const dsc& valueDsc)
+{
+ using namespace Firebird::CvtStringContains;
+ m_stringRepresentation = MOV_make_string2(tdbb, &valueDsc, JSON_TTYPE);
+
+ // m_ts = MOV_get_timestamp_tz(&valueDsc);
+ // Can't use MOV_get_timestamp_tz because the CVT_string_to_datetime function adds TimeZoneUtil::TIME_TZ_BASE_DATE
+ // and other instance stuff. It is more reliably to just use the same function instead of tring to mimic its output
+
+ Firebird::EXPECT_DATETIME format = DATETIME_SUBTYPE::expect_timestamp_tz;
+ switch (valueDsc.dsc_dtype)
+ {
+ case dtype_sql_time:
+ format = DATETIME_SUBTYPE::expect_sql_time;
+ break;
+ case dtype_sql_time_tz:
+ case dtype_ex_time_tz:
+ format = DATETIME_SUBTYPE::expect_sql_time_tz;
+ break;
+ case dtype_sql_date:
+ format = DATETIME_SUBTYPE::expect_sql_date;
+ break;
+ case dtype_timestamp:
+ format = DATETIME_SUBTYPE::expect_timestamp;
+ break;
+ case dtype_timestamp_tz:
+ case dtype_ex_timestamp_tz:
+ format = DATETIME_SUBTYPE::expect_timestamp_tz;
+ break;
+ default:
+ fb_assert(false);
+ }
+
+ m_ts = convertToTimeStamp(m_stringRepresentation, format, m_flags);
+}
+
+void JsonDatetime::updatePrecision(const USHORT precision)
+{
+ using namespace Firebird;
+
+ TimeStamp::round_time(m_ts.utc_timestamp.timestamp_time, precision);
+
+ DatetimeUtils maker;
+ dsc timestampDsc = maker.makeDatetimeDsc(m_ts, m_flags);
+
+ m_stringRepresentation = MOV_make_string2(JRD_get_thread_data(), ×tampDsc, JSON_TTYPE);
+ m_stringRepresentation.rtrim();
+}
+
+std::string_view JsonDatetime::getTypeName() const
+{
+ using namespace std::literals;
+ using namespace Firebird::CvtStringContains;
+ using namespace Firebird;
+
+ switch (getFormat())
+ {
+ case DATETIME_SUBTYPE::expect_sql_time_tz:
+ return Tokens::TypeName::TIME_WITH_TZ;
+ case DATETIME_SUBTYPE::expect_sql_time:
+ return Tokens::TypeName::TIME_WITHOUT_TZ;
+ case DATETIME_SUBTYPE::expect_timestamp_tz:
+ return Tokens::TypeName::TIMESTAMT_WITH_TZ;
+ case DATETIME_SUBTYPE::expect_timestamp:
+ return Tokens::TypeName::TIMESTAMT_WITHOUT_TZ;
+ case DATETIME_SUBTYPE::expect_sql_date:
+ return Tokens::TypeName::DATE;
+ default:
+ fb_assert(false);
+ return Tokens::TypeName::UNKNOWN;
+ }
+}
+
+Firebird::EXPECT_DATETIME JsonDatetime::getFormat() const
+{
+ return DatetimeUtils::getFormat(m_flags);
+}
+
+void JsonDatetime::storeAsBytes(Firebird::BlrWriter& array) const
+{
+ array.appendBytes(reinterpret_cast(&m_ts), TIMEZONE_SIZE);
+ array.appendMetaString(m_stringRepresentation.c_str());
+ array.appendUChar(m_flags);
+}
+
+void JsonDatetime::readFromBytes(Firebird::BlrReader& array)
+{
+ const UCHAR* pos = array.getPos();
+ array.seekForward(TIMEZONE_SIZE);
+ m_ts = *reinterpret_cast(pos);
+
+ array.getString(m_stringRepresentation);
+ m_flags = static_cast(array.getByte());
+}
+
+bool JsonDatetime::operator>(const JsonDatetime& right) const
+{
+ return Firebird::TimeZoneUtil::compareUtcTimeStamps(m_ts.utc_timestamp, right.m_ts.utc_timestamp) > 0;
+}
+
+bool JsonDatetime::operator>=(const JsonDatetime& right) const
+{
+ return Firebird::TimeZoneUtil::compareUtcTimeStamps(m_ts.utc_timestamp, right.m_ts.utc_timestamp) >= 0;
+}
+
+bool JsonDatetime::operator==(const JsonDatetime& right) const
+{
+ return Firebird::TimeZoneUtil::compareUtcTimeStamps(m_ts.utc_timestamp, right.m_ts.utc_timestamp) == 0;
+}
+
+bool JsonDatetime::operator<(const JsonDatetime& right) const
+{
+ return !operator>=(right);
+}
+
+bool JsonDatetime::operator<=(const JsonDatetime& right) const
+{
+ return !operator>(right);
+}
+
+bool JsonDatetime::operator!=(const JsonDatetime& right) const
+{
+ return !operator==(right);
+}
+
+
+ISC_TIMESTAMP_TZ JsonDatetime::convertToTimeStamp(const std::string_view source, const std::string_view format,
+ Flags& outFlags, Firebird::Callbacks* callback)
+{
+ using namespace Firebird;
+
+ fb_assert(source.length() < MAX_USHORT);
+
+ outFlags = Firebird::CvtStringContains::NONE;
+
+ dsc strdesc;
+ strdesc.makeText(static_cast(source.length()), CS_NONE, (UCHAR*)source.data());
+
+ // CVT_string_to_datetime throws exceptions not only via callback, so setting a custom ErrorFunction is not an option
+ if (callback == nullptr)
+ callback = &jsonCallback;
+
+ ISC_TIMESTAMP_TZ dest = {};
+
+ // CVT functions return timestamp in UTC
+ if (format.length())
+ {
+ // Expected type does not matter
+ // Exceptions only on invalid input
+ dest = CVT_format_string_to_datetime(&strdesc,
+ string(format.data(), static_cast(format.length())),
+ DATETIME_SUBTYPE::expect_timestamp_tz, callback, &outFlags);
+ }
+ else
+ {
+ // We need to detect the type so first try to parse the string as timestamp
+ // On failure - try sql_time
+ // If format is invalid - catch the exception in the JsonVariant::convertToDatetime method
+ try
+ {
+ CVT_string_to_datetime(&strdesc, &dest, &outFlags,
+ EXPECT_DATETIME::expect_timestamp_tz, true, callback);
+ }
+ catch (...)
+ {
+ dest = {};
+ CVT_string_to_datetime(&strdesc, &dest, &outFlags,
+ Firebird::EXPECT_DATETIME::expect_sql_time_tz, true, callback);
+ }
+
+ // When passing incorrect expected type (for example, expect_timestamp_tz for a date),
+ // the source type will be adjusted to the expected
+ // Use flags to get the real output type and extract correct value
+ const auto realType = DatetimeUtils::getFormat(outFlags);
+ dest = {};
+ CVT_string_to_datetime(&strdesc, &dest, &outFlags, realType, true, callback);
+ }
+
+ return dest;
+}
+
+ISC_TIMESTAMP_TZ JsonDatetime::convertToTimeStamp(const std::string_view source, const Firebird::EXPECT_DATETIME format,
+ Flags& outFlags, Firebird::Callbacks* callback)
+{
+ using namespace Firebird;
+
+ fb_assert(source.length() < MAX_USHORT);
+
+ outFlags = Firebird::CvtStringContains::NONE;
+
+ // Pass string as dsc
+ dsc strdesc;
+ strdesc.makeText(static_cast(source.length()), CS_NONE, (UCHAR*)source.data());
+
+ // CVT_string_to_datetime throws exceptions not only via callback, so setting a custom ErrorFunction is not an option
+ if (callback == nullptr)
+ callback = &jsonCallback;
+
+ ISC_TIMESTAMP_TZ timeStamp = {};
+ CVT_string_to_datetime(&strdesc, &timeStamp, &outFlags, format, true, callback);
+
+ return timeStamp;
+}
+
+}
diff --git a/src/jrd/json/classes/JsonDatetime.h b/src/jrd/json/classes/JsonDatetime.h
new file mode 100644
index 00000000000..b90104d1449
--- /dev/null
+++ b/src/jrd/json/classes/JsonDatetime.h
@@ -0,0 +1,138 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonDatetime.h
+ * DESCRIPTION: Common types for JSON code.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2024 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_DATETIME_H
+#define JSON_DATETIME_H
+
+#include "firebird.h"
+#include "fb_exception.h"
+#include "../../common/cvt.h"
+
+#include "../JsonConsts.h"
+
+#include
+
+namespace Firebird {
+ // Use this for serialization because they have helpful put/get fuctions
+ class BlrReader;
+ class BlrWriter;
+}
+
+namespace Jrd {
+ class thread_db;
+}
+
+namespace FBJSON {
+
+class JsonDatetime
+{
+public:
+ using Flags = Firebird::CvtStringContains::TypeFlags;
+
+ JsonDatetime(Firebird::MemoryPool& pool) : m_stringRepresentation(pool)
+ { }
+
+ JsonDatetime(Firebird::MemoryPool& pool, const JsonDatetime& rhs) : m_stringRepresentation(pool)
+ {
+ m_stringRepresentation = rhs.m_stringRepresentation;
+ m_ts = rhs.m_ts;
+ m_flags = rhs.m_flags;
+ }
+
+ void set(const std::string_view source, std::string_view format = "", Firebird::Callbacks* callback = nullptr)
+ {
+ m_ts = convertToTimeStamp(source, format, m_flags, callback);
+ m_stringRepresentation = source;
+ }
+
+ void set(SmallString&& source, const std::string_view format)
+ {
+ m_ts = convertToTimeStamp(source, format, m_flags);
+ m_stringRepresentation = std::move(source);
+ }
+
+ void set(Jrd::thread_db* tdbb, const dsc& valueDsc);
+
+ void set(const std::string_view source, const Firebird::EXPECT_DATETIME format)
+ {
+ m_ts = convertToTimeStamp(source, format, m_flags);
+ m_stringRepresentation = source;
+ }
+
+ void updateFormat(const std::string_view format, Firebird::Callbacks* callback = nullptr)
+ {
+ m_ts = convertToTimeStamp(m_stringRepresentation, format, m_flags, callback);
+ }
+
+ void updatePrecision(const USHORT precision);
+
+ SmallString&& extractString()
+ {
+ return std::move(m_stringRepresentation);
+ }
+
+ std::string_view asString() const
+ {
+ return m_stringRepresentation;
+ }
+
+ std::string_view getTypeName() const;
+ inline ISC_TIMESTAMP_TZ getTS() const noexcept
+ {
+ return m_ts;
+ }
+
+ Firebird::EXPECT_DATETIME getFormat() const;
+ void storeAsBytes(Firebird::BlrWriter& array) const;
+ void readFromBytes(Firebird::BlrReader& array);
+
+ bool operator>(const JsonDatetime& right) const;
+ bool operator>=(const JsonDatetime& right) const;
+ bool operator==(const JsonDatetime& right) const;
+ bool operator<(const JsonDatetime& right) const;
+ bool operator<=(const JsonDatetime& right) const;
+ bool operator!=(const JsonDatetime& right) const;
+
+private:
+ static constexpr FB_SIZE_T TIMEZONE_SIZE = sizeof(ISC_TIMESTAMP_TZ);
+
+private:
+ static ISC_TIMESTAMP_TZ convertToTimeStamp(const std::string_view source, const std::string_view format,
+ Flags& flags, Firebird::Callbacks* callback = nullptr);
+
+ static ISC_TIMESTAMP_TZ convertToTimeStamp(const std::string_view source, const Firebird::EXPECT_DATETIME format,
+ Flags& flags, Firebird::Callbacks* callback = nullptr);
+
+ SmallString m_stringRepresentation;
+
+ // The TS contains datetime in UTC and the source timezone
+ ISC_TIMESTAMP_TZ m_ts = {};
+ Flags m_flags = Firebird::CvtStringContains::NONE;
+};
+
+}
+
+#endif // JSON_DATETIME_H
diff --git a/src/jrd/json/classes/JsonScalar.cpp b/src/jrd/json/classes/JsonScalar.cpp
new file mode 100644
index 00000000000..18ab6b610e5
--- /dev/null
+++ b/src/jrd/json/classes/JsonScalar.cpp
@@ -0,0 +1,253 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonScalar.cpp
+ * DESCRIPTION: A scalar variant type used in JSON code.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2026 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+
+#include "JsonScalar.h"
+
+#include "../JsonConsts.h"
+#include "JsonTypes.h"
+#include "JsonDatetime.h"
+
+#include "firebird.h"
+#include "../common/classes/BlrReader.h"
+#include "../common/classes/BlrWriter.h"
+
+using namespace FBJSON;
+
+
+JsonScalar& JsonScalar::operator=(const JsonScalar& rhs)
+{
+ reset(rhs.type);
+ flags = rhs.flags;
+
+ switch (rhs.type)
+ {
+ case ValueType::STRING:
+ value.string = FB_NEW_POOL(getPool()) SmallString(getPool(), *rhs.value.string);
+ break;
+ case ValueType::DATETIME:
+ value.datetime = FB_NEW_POOL(getPool()) JsonDatetime(getPool(), *rhs.value.datetime);
+ break;
+ default:
+ memcpy(&value, &rhs.value, sizeof(DataVariant));
+ break;
+ }
+ return *this;
+}
+
+double JsonScalar::getDouble() const
+{
+ switch (type)
+ {
+ case ValueType::INT:
+ return static_cast(value.integer);
+ case ValueType::DOUBLE:
+ return value.doubleValue;
+ case ValueType::STRING:
+ json_skippable_exception::raise(JsonStatusMsg(isc_jdyn_common) <<
+ JsonStatusMsg(isc_jdyn_get_double_string_type_error) << JsonStatusMsgStrArg(*value.string));
+ default:
+ json_skippable_exception::raise(JsonStatusMsg(isc_jdyn_common) <<
+ JsonStatusMsg(isc_jdyn_get_double_invalid_type_error));
+ }
+
+ // Just in case
+ fb_assert(false);
+ return 0;
+}
+
+SmallString* JsonScalar::str() const
+{
+ if (type == ValueType::STRING)
+ return value.string;
+ else
+ {
+ fb_assert(false);
+ json_skippable_exception::raise(JsonStatusMsg(isc_jdyn_common) <<
+ JsonStatusMsg(isc_jdyn_internal_error));
+ return nullptr;
+ }
+}
+
+void JsonScalar::set(const std::string_view view)
+{
+ // Create the new string first in case the new range belongs to the current string
+ SmallString* temp = FB_NEW_POOL(getPool()) SmallString(getPool(), view.data(), static_cast(view.length()));
+ reset(ValueType::STRING);
+
+ value.string = temp;
+}
+
+JsonDatetime& JsonScalar::setToDatetime()
+{
+ reset(ValueType::DATETIME);
+
+ value.datetime = FB_NEW_POOL(getPool()) JsonDatetime(getPool());
+ return *value.datetime;
+}
+
+void JsonScalar::writeScalarAsBytes(Firebird::BlrWriter& array) const
+{
+ static constexpr USHORT ushortSizeInBits = sizeof(USHORT) * CHAR_BIT;
+ array.appendUChar(static_cast(type));
+
+ switch (type)
+ {
+ case ValueType::JNULL:
+ break;
+ case ValueType::BOOL:
+ array.appendUChar(value.boolean ? 1 : 0);
+ break;
+ case ValueType::INT:
+ array.appendUShort(value.integer);
+ array.appendUShort(value.integer >> ushortSizeInBits);
+ array.appendUShort(value.integer >> ushortSizeInBits * 2);
+ array.appendUShort(value.integer >> ushortSizeInBits * 3);
+ break;
+ case ValueType::DOUBLE:
+ {
+ SINT64 i64value = *((SINT64*)(&value.doubleValue));
+ array.appendUShort(i64value);
+ array.appendUShort(i64value >> ushortSizeInBits);
+ array.appendUShort(i64value >> ushortSizeInBits * 2);
+ array.appendUShort(i64value >> ushortSizeInBits * 3);
+ break;
+ }
+ case ValueType::STRING:
+ array.appendString(1, *value.string);
+ break;
+ case ValueType::DATETIME:
+ value.datetime->storeAsBytes(array);
+ break;
+ default:
+ fb_assert(false);
+ break;
+ }
+}
+
+void JsonScalar::readScalarFromBytes(Firebird::BlrReader& array)
+{
+ const ValueType rtype = static_cast(array.getByte());
+
+ switch (rtype)
+ {
+ case ValueType::JNULL:
+ break;
+ case ValueType::BOOL:
+ set(array.getByte() == 1);
+ break;
+ case ValueType::INT:
+ {
+ const SSHORT len = sizeof(SINT64);
+ SINT64 buffer = isc_portable_integer(array.getPos(), len);
+ array.seekForward(len);
+
+ set(buffer);
+ break;
+ }
+ case ValueType::DOUBLE:
+ {
+ const SSHORT len = sizeof(SINT64);
+ SINT64 buffer = isc_portable_integer(array.getPos(), len);
+ array.seekForward(len);
+
+ double dval = *((double*)(&buffer));
+ set(dval);
+ break;
+ }
+ case ValueType::STRING:
+ {
+ set("");
+ array.getVerbString(*value.string);
+ break;
+ }
+ case ValueType::DATETIME: // keep it as the last scalar type
+ {
+ FBJSON::JsonDatetime& datetime = setToDatetime();
+ datetime.readFromBytes(array);
+ break;
+ }
+ default:
+ fb_assert(false);
+ }
+}
+
+Jrd::impure_value JsonScalar::makeScalarDsc() const
+{
+ Jrd::impure_value output = {};
+ switch (type)
+ {
+ case ValueType::JNULL:
+ output.vlu_desc.setNull();
+ break;
+ case ValueType::BOOL:
+ output.vlu_misc.vlu_uchar = value.boolean;
+ output.vlu_desc.makeBoolean(reinterpret_cast(&output.vlu_misc.vlu_uchar));
+ break;
+ case ValueType::INT:
+ output.make_int64(value.integer);
+ break;
+ case ValueType::DOUBLE:
+ output.make_double(value.doubleValue);
+ break;
+ case ValueType::STRING:
+ output.vlu_desc.makeText(value.string->length(), JSON_TTYPE, (UCHAR*)value.string->data());
+ break;
+ case ValueType::DATETIME: // keep it as the last scalar type
+ output.vlu_misc.vlu_timestamp_tz = value.datetime->getTS();
+ output.vlu_desc.makeTimestampTz(&output.vlu_misc.vlu_timestamp_tz);
+ break;
+ default:
+ fb_assert(false);
+ }
+
+ return output;
+}
+
+void JsonScalar::reset(const ValueType newType) noexcept
+{
+ switch (type)
+ {
+ case ValueType::EMPTY:
+ case ValueType::BOOL:
+ case ValueType::INT:
+ case ValueType::JNULL:
+ break;
+ case ValueType::STRING:
+ delete value.string;
+ break;
+ case ValueType::DATETIME:
+ delete value.datetime;
+ break;
+ default:
+ releaseExtend();
+ break;
+ }
+ flags = FLAG_NONE;
+
+ value = {};
+ type = newType;
+}
diff --git a/src/jrd/json/classes/JsonScalar.h b/src/jrd/json/classes/JsonScalar.h
new file mode 100644
index 00000000000..2735594be1f
--- /dev/null
+++ b/src/jrd/json/classes/JsonScalar.h
@@ -0,0 +1,306 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonScalar.h
+ * DESCRIPTION: A scalar variant type used in JSON code.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2026 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+
+#ifndef JSON_SCALAR_H
+#define JSON_SCALAR_H
+
+#include "../JsonConsts.h"
+
+#include "../jrd/val.h"
+
+#include
+
+namespace Firebird {
+ // Use this for serialization because they have helpful put/get fuctions
+ class BlrReader;
+ class BlrWriter;
+}
+
+namespace FBJSON {
+
+class JsonDatetime;
+class OutputJsonText;
+class Jsonb;
+class JsonbView;
+
+// All possible types during runtime calculation
+enum class ValueType : UCHAR
+{
+ EMPTY = 0,
+ ERROR_VALUE,
+ UNKNOWN,
+
+ // * JsonScalar scope begin
+ JNULL,
+ BOOL,
+ INT,
+ //JFLOAT,
+ DOUBLE,
+ STRING,
+ DATETIME, // keep it as the last scalar type
+ // * JsonScalar scope end
+
+ JSON_TEXT,
+ JSON_BINARY,
+ JSON_BINARY_VIEW
+};
+constexpr std::pair SCALAR_WITH_NULL_TYPES_RANGE = {ValueType::JNULL, ValueType::DATETIME};
+constexpr std::pair SCALAR_TYPES_RANGE = {ValueType::BOOL, ValueType::DATETIME};
+
+// All possible values to store during runtime calculation
+union DataVariant
+{
+ // Scalars
+ bool boolean;
+ SINT64 integer;
+ double doubleValue;
+ SmallString* string;
+ JsonDatetime* datetime;
+
+ // Runtime values, not used in JsonScalar
+ //OutputJsonText* json; // temp comment
+ //Jsonb* jsonb; // temp comment
+ //JsonbView* jsonView; // temp comment
+};
+static_assert(sizeof(DataVariant) <= 8, "Big types should be stored as pointers to save memory");
+
+// A storage to keep parsed JSON scalar and NULL value
+// The scalar has the ability to be stored in a byted representation
+class JsonScalar : public Firebird::PermanentStorage
+{
+public:
+
+ enum Flags : UCHAR
+ {
+ FLAG_NONE = 0,
+ FLAG_HAS_QUOTES = 1
+ };
+
+ class StringView;
+
+public:
+ JsonScalar(MemoryPool& pool) : Firebird::PermanentStorage(pool)
+ { }
+
+ JsonScalar(JsonScalar&& rhs) : Firebird::PermanentStorage(rhs.getPool())
+ {
+ operator=(std::forward(rhs));
+ }
+
+ JsonScalar(const JsonScalar& rhs) : Firebird::PermanentStorage(rhs.getPool())
+ {
+ operator=(rhs);
+ }
+
+ JsonScalar(MemoryPool& pool, const JsonScalar& rhs) : Firebird::PermanentStorage(pool)
+ {
+ operator=(rhs);
+ }
+
+ JsonScalar& operator=(const JsonScalar& rhs);
+
+ JsonScalar& operator=(JsonScalar&& rhs)
+ {
+ type = rhs.type;
+ flags = rhs.flags;
+ memcpy(&value, &rhs.value, sizeof(DataVariant));
+ rhs.value = {};
+
+ rhs.type = ValueType::JNULL;
+ return *this;
+ }
+
+ virtual ~JsonScalar()
+ {
+ reset(ValueType::JNULL);
+ }
+
+public: // Getters
+ const DataVariant& getValue() const noexcept
+ {
+ return value;
+ };
+
+ DataVariant& getValue() noexcept
+ {
+ return value;
+ };
+
+
+ constexpr ValueType getType() const noexcept
+ {
+ return type;
+ }
+
+ double getDouble() const;
+
+ inline std::string_view getStringView() const
+ {
+ SmallString* asString = str();
+ return std::string_view(asString->data(), asString->length());
+ }
+
+ SmallString* str() const;
+
+public: // Checkers
+ constexpr bool hasFlag(const Flags flag) const noexcept
+ {
+ return flags & flag;
+ }
+
+ constexpr bool isScalar() const noexcept
+ {
+ return SCALAR_TYPES_RANGE.first <= type && type <= SCALAR_TYPES_RANGE.second;
+ }
+
+ constexpr bool isScalarOrNull() const noexcept
+ {
+ return SCALAR_WITH_NULL_TYPES_RANGE.first <= type && type <= SCALAR_WITH_NULL_TYPES_RANGE.second;
+ }
+
+ inline bool isNumber() const noexcept
+ {
+ return type == ValueType::INT || type == ValueType::DOUBLE;
+ }
+
+ constexpr bool isString() const noexcept
+ {
+ return type == ValueType::STRING;
+ }
+
+ constexpr bool isNull() const noexcept
+ {
+ return type == ValueType::JNULL;
+ }
+
+ constexpr bool isTrue() const noexcept
+ {
+ return type == ValueType::BOOL && value.boolean;
+ }
+
+public: // Setters
+ constexpr void addFlag(const Flags flag) noexcept
+ {
+ flags |= flag;
+ }
+
+ inline void setToNull()
+ {
+ reset(ValueType::JNULL);
+ }
+
+ inline void set(const bool boolValue)
+ {
+ reset(ValueType::BOOL);
+ value.boolean = boolValue;
+ }
+
+ inline void set(const int intValue)
+ {
+ reset(ValueType::INT);
+ value.integer = intValue;
+ }
+
+ inline void set(const SINT64 bigIntValue)
+ {
+ reset(ValueType::INT);
+ value.integer = bigIntValue;
+ }
+
+ inline void set(const double doublevalue)
+ {
+ reset(ValueType::DOUBLE);
+ value.doubleValue = doublevalue;
+ }
+
+ //! Add explicit overload for c-string to avoid casting const char* to bool
+ void set(const char* const view)
+ {
+ set(std::string_view(view));
+ }
+
+ void set(const std::string_view view);
+
+ //! Remove random pointer to bool casts
+ template
+ void set(T*) = delete;
+
+ JsonDatetime& setToDatetime();
+
+public: // Serialization
+ void writeScalarAsBytes(Firebird::BlrWriter& array) const;
+ void readScalarFromBytes(Firebird::BlrReader& array);
+
+ Jrd::impure_value makeScalarDsc() const;
+
+protected:
+ void reset(const ValueType newType) noexcept;
+
+ // Used in JsonVariant to release json/jsonb pointers
+ virtual void releaseExtend()
+ { }
+
+ // NOLINTBEGIN(misc-non-private-member-variables-in-classes)
+protected:
+ DataVariant value = {};
+ ValueType type = ValueType::JNULL;
+ UCHAR flags = FLAG_NONE; // Only runtime
+ // NOLINTEND(misc-non-private-member-variables-in-classes)
+};
+
+class JsonScalar::StringView
+{
+public:
+ StringView(MemoryPool& pool, SmallString* stringToSet) :
+ m_scalar(pool)
+ {
+ m_scalar.value.string = stringToSet;
+ m_scalar.type = ValueType::STRING;
+ }
+
+ StringView(const StringView& ref) = delete;
+ StringView(StringView&& ref) = delete;
+
+ auto operator=(const StringView&) = delete;
+ auto operator=(StringView&&) = delete;
+
+ ~StringView()
+ {
+ m_scalar.type = ValueType::EMPTY;
+ }
+
+ const JsonScalar& operator*() noexcept
+ {
+ return m_scalar;
+ }
+
+private:
+ JsonScalar m_scalar;
+};
+
+}
+#endif
diff --git a/src/jrd/json/classes/JsonTypes.cpp b/src/jrd/json/classes/JsonTypes.cpp
new file mode 100644
index 00000000000..ae0d1f69c6a
--- /dev/null
+++ b/src/jrd/json/classes/JsonTypes.cpp
@@ -0,0 +1,462 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonTypes.cpp
+ * DESCRIPTION: Common types used in JSON code.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2024 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#include "JsonTypes.h"
+
+#include "../dsql/chars.h"
+#include
+
+#ifdef HAVE_FLOAT_H
+#include
+#else
+static inline constexpr DBL_MAX_10_EXP = 308
+#endif
+
+#include "unicode/utf16.h" // U16_IS_TRAIL
+
+using namespace FBJSON;
+
+static constexpr FB_UINT64 LIMIT_BY_10 = MAX_SINT64 / 10;
+static constexpr USHORT UNICODE_HEX_SEQUENCE_LENGTH = 4; // XXXXX
+static constexpr USHORT UNICODE_ESCAPED_SEQUENCE_LENGTH = 6; // \uXXXX - with the escape
+static constexpr int LEAD_SURROGATE_NULL = -1;
+
+static inline bool addToValue(SINT64& value, const char c, const bool hasMinus)
+{
+ if ((static_cast(value) > LIMIT_BY_10) || (value == LIMIT_BY_10 && c > '7'))
+ {
+ if (!hasMinus || value != LIMIT_BY_10 || c != '8')
+ return false;
+ }
+
+ value = value * 10 + (c - '0');
+ return true;
+}
+
+// Exceptions
+
+json_skippable_exception::json_skippable_exception(const ISC_STATUS *status_vector) throw()
+ : status_exception(status_vector)
+{ }
+
+json_skippable_exception::json_skippable_exception(const json_skippable_exception& rhs)
+ : status_exception(rhs)
+{ }
+
+void json_skippable_exception::raise(const JsonStatusVector& statusVector)
+{
+ throw json_skippable_exception(statusVector.value());
+}
+
+
+json_fatal_exception::json_fatal_exception(const ISC_STATUS *status_vector) throw()
+ : status_exception(status_vector)
+{ }
+
+json_fatal_exception::json_fatal_exception(const json_fatal_exception& rhs)
+ : status_exception(rhs)
+{ }
+
+
+void json_fatal_exception::raise(const JsonStatusVector& statusVector)
+{
+ throw json_fatal_exception(statusVector.value());
+}
+
+// Classes
+
+template
+void FBJSON::JsonScalarParser::parseQuotedString(TView& input, TextPos& current, SmallString& escapelessOutput)
+{
+ escapelessOutput.clear();
+ escapelessOutput.reserve(BUFFER_SMALL);
+
+ const TextPos end = input.end();
+
+ // UTF-16, two \uXXXX in a row
+ int leadSurrogate = LEAD_SURROGATE_NULL;
+
+ while (current < end)
+ {
+ char c = input[current++];
+
+ if (c == '"')
+ {
+ return;
+ }
+ else if ((unsigned char)c < 32)
+ {
+ escapelessOutput.printf("%02x", (unsigned char)c);
+ initError() << JsonStatusMsg(isc_jparser_unescaped_character) << escapelessOutput;
+
+ escapelessOutput.clear();
+ return;
+ }
+ else if (c == '\\')
+ {
+ if (current == end)
+ {
+ initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string);
+
+ escapelessOutput.clear();
+ return;
+ }
+
+ c = input[current++];
+
+ if (c == 'u')
+ {
+ // Unicode escape sequence \uXXXX
+
+ if (current + UNICODE_HEX_SEQUENCE_LENGTH >= end)
+ {
+ initError() << JsonStatusMsg(isc_jparser_invalid_sequence) << input.getErrorSubstring(current, end - current);
+ current = end;
+
+ escapelessOutput.clear();
+ return;
+ }
+
+ // Get a digital code of the handling hex sequence to store it to hexToUnicodeValue
+ int hexToUnicodeValue = 0;
+ for (USHORT i = 0; i < UNICODE_HEX_SEQUENCE_LENGTH; ++i)
+ {
+ c = input[current++];
+ const auto cherMeta = classes_array[static_cast(c)];
+ if (cherMeta & CHR_DIGIT)
+ {
+ hexToUnicodeValue = (hexToUnicodeValue << 4) + (c - '0');
+ }
+ else
+ {
+ c = UPPER(c);
+ if (cherMeta & CHR_HEX)
+ {
+ hexToUnicodeValue = (hexToUnicodeValue << 4) + (c - 'A') + 10;
+ }
+ else
+ {
+ initError() << JsonStatusMsg(isc_jparser_un_incorrect_code) <<
+ input.getErrorSubstring(current - i - 1, UNICODE_HEX_SEQUENCE_LENGTH);
+ return;
+ }
+ }
+ }
+
+ // The first (lead) surrogate is a 16-bit code value in the range U+D800 to U+DBFF
+ if (U16_IS_LEAD(hexToUnicodeValue)) // or U_IS_LEAD
+ {
+ if (leadSurrogate != LEAD_SURROGATE_NULL)
+ {
+ // Two lead codes in a row
+ initError() << JsonStatusMsg(isc_jparser_un_double_high_surrogate) <<
+ input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH);
+ return;
+ }
+ leadSurrogate = hexToUnicodeValue;
+ continue;
+ }
+ // The second (tail) surrogate is a 16-bit code value in the range U+DC00 to U+DFFF
+ else if (U16_IS_TRAIL(hexToUnicodeValue)) // or U_IS_TRAIL
+ {
+ if (leadSurrogate == LEAD_SURROGATE_NULL)
+ {
+ initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) <<
+ input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH);
+ return;
+ }
+ hexToUnicodeValue = U16_GET_SUPPLEMENTARY(leadSurrogate, hexToUnicodeValue);
+ }
+ else if (leadSurrogate != LEAD_SURROGATE_NULL)
+ {
+ initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) <<
+ input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH);
+ return;
+ }
+
+ if (hexToUnicodeValue == 0)
+ {
+ initError() << JsonStatusMsg(isc_jparser_un_conv_error);
+ return;
+ }
+ // C0 Controls and Basic Latin Range: 0000–007F (127)
+ else if (hexToUnicodeValue <= CHAR_MAX)
+ {
+ escapelessOutput.append(1, (char)hexToUnicodeValue);
+ }
+ else
+ {
+ // Append the raw code
+
+ // We are standing to the last hex digit so start and end with offset of 1
+ const TextPos unicodeEndPos = current;
+
+ // We need to get the full code sequence including the escape
+ if (leadSurrogate != LEAD_SURROGATE_NULL)
+ {
+ for (TextPos i = unicodeEndPos - (2 * UNICODE_ESCAPED_SEQUENCE_LENGTH); i < unicodeEndPos; ++i)
+ {
+ escapelessOutput.append(1, input[i]);
+ }
+
+ // Two codes, 12 chars - \uXXXX\uXXXX
+ leadSurrogate = LEAD_SURROGATE_NULL;
+ }
+ else // 6 chars - \uXXXX
+ {
+ for (TextPos i = unicodeEndPos - UNICODE_ESCAPED_SEQUENCE_LENGTH; i < unicodeEndPos; ++i)
+ {
+ escapelessOutput.append(1, input[i]);
+ }
+ }
+ }
+
+ continue; // End of unicode sequence handling
+ }
+
+ switch (c)
+ {
+ case '\'':
+ case '"':
+ case '\\':
+ case '/':
+ escapelessOutput.append(1, c);
+ break;
+ case 'b':
+ escapelessOutput.append(1, '\b');
+ break;
+ case 'f':
+ escapelessOutput.append(1, '\f');
+ break;
+ case 'n':
+ escapelessOutput.append(1, '\n');
+ break;
+ case 'r':
+ escapelessOutput.append(1, '\r');
+ break;
+ case 't':
+ escapelessOutput.append(1, '\t');
+ break;
+ default:
+ escapelessOutput = c;
+ initError() << JsonStatusMsg(isc_jparser_invalid_sequence) << JsonStatusMsgStrArg(escapelessOutput);
+ return;
+ } // !switch
+ }
+ else
+ {
+ // Handle a simple character
+ escapelessOutput.append(1, c);
+ }
+
+ // The unicode tail is missing
+ // Handle a non-unicode escape character
+ if (leadSurrogate != LEAD_SURROGATE_NULL)
+ {
+ initError() << JsonStatusMsg(isc_jparser_un_missing_high_surrogate) <<
+ input.getErrorSubstring(current - UNICODE_HEX_SEQUENCE_LENGTH, UNICODE_HEX_SEQUENCE_LENGTH);
+ return;
+ }
+
+ } // for
+
+ if (current >= end)
+ {
+ initError() << JsonStatusMsg(isc_jparser_invalid_quoted_string);
+ }
+}
+
+
+template
+ParsedNumber FBJSON::JsonScalarParser::parseNumber(TView& input, TextPos& current, bool strictMode)
+{
+ const TextPos start = current;
+ const TextPos end = input.end();
+
+ ParsedNumber lex;
+ lex.value = 0;
+
+ if (strictMode)
+ {
+ if (current == end)
+ {
+ initError() << JsonStatusMsg(isc_jparser_empty_input);
+ return lex;
+ }
+ }
+
+ // Skip spaces, catch first sign
+ bool hasUnaryMinus = false;
+ for (; current < end; ++current)
+ {
+ const char c = input[current];
+ switch (c)
+ {
+ case ' ':
+ continue;
+ case '-':
+ ++current;
+ hasUnaryMinus = true;
+ break;
+ case '+':
+ default:
+ break;
+ }
+
+ break;
+ }
+
+ // Delay overflow error print to lex the full number range and put it into error
+ bool hasOverflow = false;
+
+ // First part
+ for (; current < end; ++current)
+ {
+ const char c = input[current];
+
+ if (!(classes(c) & CHR_DIGIT))
+ break;
+
+ if (!addToValue(lex.value, c, hasUnaryMinus))
+ {
+ hasOverflow = true;
+ }
+ }
+
+ // Decimal part
+ if (current < end && input[current] == '.')
+ {
+ ++current;
+ lex.isDouble = true;
+ for (; current < end; ++current)
+ {
+ const char c = input[current];
+ if (!(classes(c) & CHR_DIGIT))
+ break;
+
+ if (!addToValue(lex.value, c, hasUnaryMinus))
+ {
+ hasOverflow = true;
+ }
+
+ --lex.scale;
+ }
+ }
+
+ if (lex.scale < MIN_SCHAR || lex.scale > MAX_SCHAR)
+ {
+ hasOverflow = true;
+ }
+
+ // Exponent sign
+ SLONG expValue = 0;
+ if (current < end && (input[current] == 'e' || input[current] == 'E'))
+ {
+ lex.isDouble = true;
+
+ if (++current < end)
+ {
+ const char next = input[current];
+
+ bool haveExponentSign = false;
+ if (next == '+')
+ {
+ ++current;
+ haveExponentSign = false;
+ }
+ else if (next == '-')
+ {
+ ++current;
+ haveExponentSign = true;
+ }
+
+ for (; current < end; ++current)
+ {
+ const char c = input[current];
+ if (!(classes(c) & CHR_DIGIT))
+ break;
+
+ expValue = expValue * 10 + (c - '0');
+ }
+
+ if (expValue > DBL_MAX_10_EXP)
+ {
+ hasOverflow = true;
+ }
+
+ if (haveExponentSign)
+ {
+ expValue = -expValue;
+ }
+ }
+
+ const double maxNum = DBL_MAX / static_cast(std::pow(10, expValue));
+ if (double(lex.value) > maxNum)
+ {
+ hasOverflow = true;
+ }
+
+ lex.scale += static_cast(expValue);
+ }
+
+ if (hasOverflow)
+ {
+ initError() << JsonStatusMsg(isc_jparser_number_overflow) <<
+ JsonStatusMsgStrArg(input.getErrorSubstring(start, end - start));
+
+ return lex;
+ }
+
+ if (hasUnaryMinus)
+ lex.value = -lex.value;
+
+ if (strictMode)
+ {
+ for (; current < end; ++current)
+ {
+ const char c = input[current];
+
+ if (!(classes(c) & CHR_WHITE))
+ {
+ initError() << JsonStatusMsg(isc_jparser_invalid_number_parse) <<
+ JsonStatusMsgStrArg(input.getErrorSubstring(start));
+ break;
+ }
+ }
+ }
+
+ return lex;
+}
+
+template
+JsonStatusMsg& JsonScalarParser::initError()
+{
+ error.reset(FB_NEW JsonStatusMsg());
+ return *error.get();
+}
+
+template class FBJSON::JsonScalarParser;
+// template class FBJSON::JsonScalarParser;
diff --git a/src/jrd/json/classes/JsonTypes.h b/src/jrd/json/classes/JsonTypes.h
new file mode 100644
index 00000000000..e9cb096d656
--- /dev/null
+++ b/src/jrd/json/classes/JsonTypes.h
@@ -0,0 +1,312 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonTypes.h
+ * DESCRIPTION: Common types used in JSON code.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2024 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_TYPES_H
+#define JSON_TYPES_H
+
+#include "firebird.h"
+#include "fb_exception.h"
+#include "../JsonConsts.h"
+
+namespace FBJSON {
+
+using ArraySize = ULONG;
+using RangeSize = SSHORT;
+
+// JsonPath Extention: Negative values supported
+using PathArrayIndex = SLONG;
+
+// Enums
+
+// A sub-enum for JsonType and JsonbType enums
+// The order represents compare cardinality:
+// null < SQL/JSON scalar < array < object
+enum CommonJsonType : UCHAR
+{
+ JT_EMPTY = 0, // Special type
+ JT_NULL = 1,
+ JT_SCALAR = 2,
+ JT_ARRAY = 3,
+ JT_OBJECT = 4
+};
+
+enum class JsonType : UCHAR
+{
+ Empty = JT_EMPTY, // Special type
+ Null = JT_NULL,
+ Scalar = JT_SCALAR,
+ Array = JT_ARRAY,
+ Object = JT_OBJECT
+};
+
+enum class JsonbType : UCHAR
+{
+ Empty = JT_EMPTY,
+ Scalar = JT_SCALAR, // Scalar or null
+ Array = JT_ARRAY,
+ Object = JT_OBJECT,
+ DuplicateKey
+};
+
+enum class PathMethod
+{
+ NONE = 0,
+ TYPE = 1,
+ SIZE = 2,
+ DOUBLE = 3,
+ CEILING = 4,
+ FLOOR = 5,
+ ABS = 6,
+ DATETIME = 7,
+ KEYVALUE = 8,
+
+ // Pass unary operations as methods in dynamic path to simplify things
+ // In a math expr and filter, normal dynamic operators are used
+ METHOD_UNARY_PLUS,
+ METHOD_UNARY_MINUS,
+
+ // SQL 2023 methods
+
+ STRING,
+ BOOLEAN,
+ BIGINT,
+ DECIMAL,
+ INTEGER,
+ NUMBER,
+
+ DATE,
+ TIME,
+ TIME_TZ,
+ TIMESTAMP,
+ TIMESTAMP_TZ
+};
+
+enum class PathType
+{
+ MAIN = 0, //$
+ MAIN_IN_FILTER, //$
+ ITEM, //@
+ PASSING //$
+};
+
+// Element of JSON Path
+enum class ItemType : UCHAR
+{
+ FIELD, // $.field
+ ARRAY_ELEMENT // $[0]
+};
+
+// Helpers
+
+constexpr void rtrim(std::string_view& view)
+{
+ size_t stringLength = view.length();
+ while (stringLength > 0 && view[stringLength - 1] == ' ')
+ {
+ --stringLength;
+ }
+ view.remove_suffix(view.length() - stringLength);
+}
+
+// Common types
+
+class json_skippable_exception : public Firebird::status_exception
+{
+public:
+ explicit json_skippable_exception(const ISC_STATUS *status_vector) throw();
+
+ json_skippable_exception(const json_skippable_exception& rhs);
+ json_skippable_exception(json_skippable_exception&&) noexcept = delete;
+
+ json_skippable_exception& operator=(const json_skippable_exception&) = delete;
+ json_skippable_exception& operator=(json_skippable_exception&&) noexcept = delete;
+
+ virtual ~json_skippable_exception() = default;
+
+ [[noreturn]] static void raise(const JsonStatusVector& statusVector);
+};
+
+class json_fatal_exception : public Firebird::status_exception
+{
+public:
+ explicit json_fatal_exception(const ISC_STATUS *status_vector) throw();
+
+ json_fatal_exception(const json_fatal_exception& rhs);
+ json_fatal_exception(json_fatal_exception&&) noexcept = delete;
+
+ json_fatal_exception& operator=(const json_fatal_exception&) = delete;
+ json_fatal_exception& operator=(json_fatal_exception&&) noexcept = delete;
+
+ virtual ~json_fatal_exception() = default;
+
+ [[noreturn]] static void raise(const JsonStatusVector& statusVector);
+};
+
+using json_syntax_exception = json_fatal_exception;
+using json_strict_exception = json_fatal_exception;
+
+struct JsonLevelNode
+{
+ JsonLevelNode* next = nullptr;
+ JsonLevelNode* prev = nullptr;
+
+ SmallString field;
+ ArraySize indexInArray = 0;
+ ArraySize arraySize = 0;
+ SSHORT depth = 0;
+ CommonJsonType type = JT_EMPTY;
+ ItemType itemType = ItemType::FIELD;
+
+ JsonLevelNode(MemoryPool& pool) :
+ field(pool)
+ { }
+
+ JsonLevelNode(const JsonLevelNode&) = delete;
+ JsonLevelNode(JsonLevelNode&&) noexcept = delete;
+
+ JsonLevelNode& operator=(const JsonLevelNode&) = delete;
+ JsonLevelNode& operator=(JsonLevelNode&&) noexcept = delete;
+
+ ~JsonLevelNode()
+ {
+ JsonLevelNode* current = next;
+ JsonLevelNode* curNext;
+ while (current)
+ {
+ curNext = current->next;
+ current->next = nullptr;
+ delete current;
+ current = curNext;
+ }
+ }
+
+ inline ArraySize getArraySize() const noexcept
+ {
+ // The current level is an element; jsonNode->prev is an array level
+ if (prev && prev->isArray())
+ return prev->arraySize;
+ else
+ return 1;
+ }
+
+ inline bool isArray() const noexcept
+ {
+ return type == JT_ARRAY;
+ }
+};
+
+struct ParsedNumber
+{
+ SINT64 value = 0;
+ SLONG scale = 0;
+ bool isDouble = false;
+
+ inline double getDouble() const
+ {
+ static constexpr double scaleMove = 10.0;
+
+ double result = static_cast(value);
+ if (scale > 0)
+ {
+ for (USHORT i = 0; i < scale; ++i)
+ result *= scaleMove;
+ }
+ else
+ {
+ for (USHORT i = 0; i < abs(scale); ++i)
+ result /= scaleMove;
+ }
+
+ return result;
+ }
+};
+
+
+template
+class JsonScalarParser
+{
+public:
+ // JsonStatusMsg allocation is super expansive
+ Firebird::AutoPtr error;
+
+ bool hasError() const
+ {
+ return error && error->isDirty();
+ }
+
+ void parseQuotedString(TView& view, TextPos& current, SmallString& escapelessOutput);
+
+ // strictMode for unary plus/minus and parse errors
+ ParsedNumber parseNumber(TView& input, TextPos& current, const bool strictMode);
+
+private:
+ JsonStatusMsg& initError();
+};
+
+
+struct StringParseView
+{
+ std::string_view base;
+ char operator[](TextPos i)
+ {
+ return base[i];
+ }
+
+ SmallString getErrorSubstring(TextPos subStart, TextPos subLength = JSON_MAX_REPORT_SIZE) const
+ {
+ if (subStart + subLength > base.length())
+ {
+ subLength = base.length() - subStart;
+ }
+
+ bool overload = false;
+ if (subLength > JSON_MAX_REPORT_SIZE)
+ {
+ overload = true;
+ subLength = JSON_MAX_REPORT_SIZE;
+ }
+
+ SmallString buffer(base.data() + subStart, subLength);
+ if (overload)
+ {
+ buffer += "...";
+ return buffer;
+ }
+ else
+ return buffer;
+ }
+
+ inline TextPos end() const
+ {
+ return static_cast(base.length());
+ }
+};
+
+using StringParser = JsonScalarParser;
+// using InputJsonParser = JsonScalarParser;
+
+}
+#endif
diff --git a/src/jrd/json/path/JPathParser.cpp b/src/jrd/json/path/JPathParser.cpp
new file mode 100644
index 00000000000..4a5c77d9242
--- /dev/null
+++ b/src/jrd/json/path/JPathParser.cpp
@@ -0,0 +1,615 @@
+
+/*
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2023 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#include "firebird.h"
+#include
+#include
+#include "JPathParser.h"
+#include "../JsonUtils.h"
+#include "../dsql/chars.h"
+#include "../jrd/DataTypeUtil.h"
+#include "../jrd/intl_proto.h"
+
+using namespace Firebird;
+using namespace FBJSON;
+
+
+class PathTokens
+{
+public:
+ LeftPooledMap keyword;
+ LeftPooledMap methods;
+ PathTokens(MemoryPool& p) :
+ keyword(p), methods(p)
+ {
+ keyword.put("lax", TOK_LAX);
+ keyword.put("strict", TOK_STRICT);
+ keyword.put("last", TOK_LAST);
+ keyword.put("to", TOK_TO);
+
+ keyword.put("null", TOK_JSON_NULL);
+ keyword.put("true", TOK_TRUE);
+ keyword.put("false", TOK_FALSE);
+
+ keyword.put("starts", TOK_STARTS);
+ keyword.put("with", TOK_WITH);
+ keyword.put("like_regex", TOK_LIKE_REGEX);
+ keyword.put("is", TOK_IS);
+ keyword.put("unknown", TOK_UNKNOWN);
+ keyword.put("flags", TOK_FLAGS);
+ keyword.put("exists", TOK_EXISTS);
+
+ // Process the follows methods in a special way
+ keyword.put("keyvalue", TOK_KEYVALUE);
+ keyword.put("datetime", TOK_DATETIME);
+ keyword.put("time", TOK_TIME);
+ keyword.put("time_tz", TOK_TIME_TZ);
+ keyword.put("timestamp", TOK_TIMESTAMP);
+ keyword.put("timestamp_tz", TOK_TIMESTAMP_TZ);
+ keyword.put("decimal", TOK_DECIMAL);
+
+ // Methods without an arg
+ methods.put("type", PathMethod::TYPE);
+ methods.put("size", PathMethod::SIZE);
+ methods.put("double", PathMethod::DOUBLE);
+ methods.put("ceiling", PathMethod::CEILING);
+ methods.put("floor", PathMethod::FLOOR);
+ methods.put("abs", PathMethod::ABS);
+
+ methods.put("date", PathMethod::DATE);
+ methods.put("string", PathMethod::STRING);
+ methods.put("boolean", PathMethod::BOOLEAN);
+ methods.put("bigint", PathMethod::BIGINT);
+ methods.put("integer", PathMethod::INTEGER);
+ methods.put("number", PathMethod::NUMBER);
+ }
+};
+
+GlobalPtr tokens;
+
+
+const int* getKeyword(const string& str)
+{
+ return tokens->keyword.get(str);
+}
+
+const int getNonCharKeyword(const char* str)
+{
+ switch (getSuperType(str[0], str[1]))
+ {
+ case getSuperType('|', '|'):
+ return TOK_OR;
+ case getSuperType('&', '&'):
+ return TOK_AND;
+ case getSuperType('=', '='):
+ return TOK_EQ;
+ case getSuperType('!', '='):
+ case getSuperType('<', '>'):
+ return TOK_NEQ;
+ case getSuperType('<', '='):
+ return TOK_LEQ;
+ case getSuperType('>', '='):
+ return TOK_GEQ;
+ }
+
+ return 0;
+}
+
+
+PathParser::PathParser(MemoryPool& pool, const std::string_view jsonPath) :
+ PermanentStorage(pool),
+ m_tempPool(MemoryPool::createPool())
+{
+ yyps = 0;
+ yypath = 0;
+ yylvals = 0;
+ yylvp = 0;
+ yylve = 0;
+ yylvlim = 0;
+ yylpsns = 0;
+ yylpp = 0;
+ yylpe = 0;
+ yylplim = 0;
+ yylexp = 0;
+ yylexemes = 0;
+
+ const char* begin = jsonPath.data();
+ const char* end = begin + jsonPath.length();
+
+ yyposn.firstLine = 1;
+ yyposn.firstColumn = 1;
+ yyposn.lastLine = 1;
+ yyposn.lastColumn = 1;
+ yyposn.firstPos = begin;
+ yyposn.leadingFirstPos = begin;
+ yyposn.lastPos = end;
+ yyposn.trailingLastPos = end;
+
+ lex.start = begin;
+ lex.line_start = lex.last_token = lex.ptr = lex.leadingPtr = begin;
+ lex.end = end;
+ lex.line_start_bk = lex.line_start;
+ lex.prev_keyword = -1;
+
+#ifdef DSQL_DEBUG
+ if (DSQL_debug & 32)
+ dsql_trace("Source DSQL string:\n%.*s", (int) length, string);
+#endif
+}
+
+
+PathParser::~PathParser()
+{
+ while (yyps)
+ {
+ yyparsestate* p = yyps;
+ yyps = p->save;
+ yyFreeState(p);
+ }
+
+ while (yypath)
+ {
+ yyparsestate* p = yypath;
+ yypath = p->save;
+ yyFreeState(p);
+ }
+
+ delete[] yylvals;
+ delete[] yylpsns;
+ delete[] yylexemes;
+}
+
+
+JsonPathExpr* PathParser::parse(const PassingKeys* keys)
+{
+ this->m_passingKeys = keys;
+
+ m_output.reset(FB_NEW_POOL(getPool()) JsonPathExpr(getPool()));
+
+ if (parseAux() != 0)
+ {
+ fb_assert(false);
+ }
+
+ if (!m_allowPathExtensions && m_output->hasMath())
+ {
+ json_syntax_exception::raise(
+ JsonStatusMsg(isc_jpath_common) <<
+ JsonStatusMsg(isc_jpath_multiple_roots));
+ }
+
+ return m_output.release();
+}
+
+// Set the position of a left-hand non-terminal based on its right-hand rules.
+void PathParser::yyReducePosn(YYPOSN& ret, YYPOSN* termPosns, YYSTYPE* /*termVals*/, int termNo,
+ int /*stkPos*/, int /*yychar*/, YYPOSN& /*yyposn*/, void*)
+{
+ if (termNo == 0)
+ {
+ // Accessing termPosns[-1] seems to be the only way to get correct positions in this case.
+ ret.firstLine = ret.lastLine = termPosns[termNo - 1].lastLine;
+ ret.firstColumn = ret.lastColumn = termPosns[termNo - 1].lastColumn;
+ ret.firstPos = ret.lastPos = ret.trailingLastPos = termPosns[termNo - 1].trailingLastPos;
+ ret.leadingFirstPos = termPosns[termNo - 1].lastPos;
+ }
+ else
+ {
+ ret.firstLine = termPosns[0].firstLine;
+ ret.firstColumn = termPosns[0].firstColumn;
+ ret.firstPos = termPosns[0].firstPos;
+ ret.leadingFirstPos = termPosns[0].leadingFirstPos;
+ ret.lastLine = termPosns[termNo - 1].lastLine;
+ ret.lastColumn = termPosns[termNo - 1].lastColumn;
+ ret.lastPos = termPosns[termNo - 1].lastPos;
+ ret.trailingLastPos = termPosns[termNo - 1].trailingLastPos;
+ }
+}
+
+
+int PathParser::yylex()
+{
+ if (!yylexSkipSpaces())
+ return -1;
+
+ yyposn.firstLine = 1;
+ yyposn.firstColumn = lex.ptr - lex.line_start;
+ yyposn.firstPos = lex.ptr - 1;
+ yyposn.leadingFirstPos = lex.leadingPtr;
+
+ lex.prev_keyword = yylexAux();
+
+ yyposn.lastPos = lex.ptr;
+ lex.leadingPtr = lex.ptr;
+
+ bool spacesSkipped = yylexSkipSpaces();
+
+ yyposn.lastLine = 1;
+ yyposn.lastColumn = lex.ptr - lex.line_start;
+
+ if (spacesSkipped)
+ --lex.ptr;
+
+ yyposn.trailingLastPos = lex.ptr;
+
+ return lex.prev_keyword;
+}
+
+
+bool PathParser::yylexSkipSpaces()
+{
+ UCHAR tok_class;
+ SSHORT c;
+
+ // Find end of white space
+ for (;;)
+ {
+ if (lex.ptr >= lex.end)
+ return false;
+
+ if (yylexSkipEol())
+ continue;
+
+ c = *lex.ptr++;
+ tok_class = classes(c);
+
+ if (!(tok_class & CHR_WHITE))
+ break;
+ }
+
+ return true;
+}
+
+
+bool PathParser::yylexSkipEol()
+{
+ bool eol = false;
+ const TEXT c = *lex.ptr;
+
+ if (c == '\r')
+ {
+ lex.ptr++;
+ if (lex.ptr < lex.end && *lex.ptr == '\n')
+ lex.ptr++;
+
+ eol = true;
+ }
+ else if (c == '\n')
+ {
+ lex.ptr++;
+ eol = true;
+ }
+
+ if (eol)
+ {
+ lex.line_start = lex.ptr; // + 1; // CVC: +1 left out.
+ }
+
+ return eol;
+}
+
+static inline bool isTokenChar(const char c)
+{
+ return (classes(c) & CHR_LETTER) || c == '_';
+}
+
+static inline bool isStringChar(const char c)
+{
+ return classes(c) & CHR_IDENT;
+}
+
+static inline const char* skipSpaces(const char* start, const char* end)
+{
+ const char* it = start;
+ for (; it < end && isStringChar(*it); ++it);
+
+ return it;
+}
+
+static inline const char* readToken(const char* start, const char* end)
+{
+ const char* it = start;
+ for (; it < end && isTokenChar(*it); ++it);
+
+ return it;
+}
+
+static inline const char* readString(const char* start, const char* end)
+{
+ const char* it = start;
+ for (; it < end && isStringChar(*it); ++it);
+
+ return it;
+}
+
+static inline const char* readToken(const char* start, const char* end, const char stop)
+{
+ const char* it = start;
+ for (; it < end && isTokenChar(*it) && *it != stop; ++it);
+
+ return it;
+}
+
+int PathParser::yylexAux()
+{
+ SSHORT c = lex.ptr[-1];
+ UCHAR tok_class = classes(c);
+ lex.last_token = lex.ptr - 1;
+
+ // It does not work for lex errors to set the flag in the yacc file
+ switch (lex.prev_keyword)
+ {
+ case static_cast('?'):
+ m_lexInFilter = true;
+ m_filterBracketCounter = 0;
+ break;
+ case static_cast('('):
+ ++m_filterBracketCounter;
+ break;
+ case static_cast(')'):
+ if (m_lexInFilter && --m_filterBracketCounter == 0)
+ m_lexInFilter = false;
+ break;
+ }
+
+
+ // Parse a quoted string, being sure to look for double quotes
+ if (tok_class & CHR_QUOTE)
+ {
+ StringParser scalarParser;
+ TextPos offset = 0;
+ std::string_view view(lex.ptr, lex.end - lex.ptr);
+ StringParseView viewp{view};
+ string* stringPtr = mkTempString("");
+ scalarParser.parseQuotedString(viewp, offset, *stringPtr);
+
+ lex.ptr += offset;
+ if (scalarParser.hasError())
+ {
+ raiseFullErrorVector(*scalarParser.error, yyposn);
+ }
+
+ yylval.stringPtr = stringPtr;
+ return TOK_QUOTED_STRING;
+ }
+
+ fb_assert(lex.ptr <= lex.end);
+
+ if ((tok_class & CHR_DIGIT) ||
+ ((c == '.') && (lex.ptr < lex.end) && (classes(*lex.ptr) & CHR_DIGIT)))
+ {
+ --lex.ptr;
+
+ StringParser scalarParser;
+ TextPos offset = 0;
+ std::string_view view(lex.ptr, lex.end - lex.ptr);
+ StringParseView viewp{view};
+ const ParsedNumber number = scalarParser.parseNumber(viewp, offset, false);
+
+ lex.ptr += offset;
+ if (scalarParser.hasError())
+ {
+ raiseFullErrorVector(*scalarParser.error, yyposn);
+ }
+
+ // Should we use floating point type?
+ if (number.isDouble)
+ {
+ lex.last_token_bk = lex.last_token;
+ lex.line_start_bk = lex.line_start;
+
+ yylval.doubleVal = number.getDouble();
+ return TOK_DOUBLE;
+ }
+ else
+ {
+ lex.last_token_bk = lex.last_token;
+ lex.line_start_bk = lex.line_start;
+
+ if (number.value <= MAX_SSHORT)
+ {
+ yylval.int16Val = (SSHORT) number.value;
+ return TOK_INTEGER16;
+ }
+ else if (number.value <= MAX_SLONG)
+ {
+ yylval.int32Val = (SLONG) number.value;
+ return TOK_INTEGER32;
+ }
+ else
+ {
+ yylval.int64Val = number.value;
+ return TOK_INTEGER64;
+ }
+ }
+ }
+
+ // Restore the status quo ante, before we started our unsuccessful
+ // attempt to recognize a number.
+ lex.ptr = lex.last_token;
+ c = *lex.ptr++;
+ // We never touched tok_class, so it doesn't need to be restored.
+
+ if (c == '$' && lex.last_token + 1 < lex.end && isStringChar(*lex.ptr) && *lex.ptr != '$')
+ {
+ const char* start = lex.ptr - 1;
+ lex.ptr = readString(lex.ptr, lex.end);
+
+ const string rawPassingStr(start + 1, lex.ptr - start - 1); // Skip $
+ yylval.stringPtr = mkTempString(rawPassingStr);
+ return TOK_PASSING_NAME;
+ }
+ else if (c == '.')
+ {
+ const char* methodStart = skipSpaces(lex.ptr, lex.end);
+ const char* end = readToken(methodStart, lex.end, '(');
+ if (*end == '(')
+ {
+ return TOK_METHOD_DOT;
+ }
+ }
+ else if (tok_class & CHR_LETTER)
+ {
+ // An quoteless filed name or a token or a passing variable
+
+ // First get only token chars
+ const char* start = lex.ptr - 1;
+ lex.ptr = readToken(lex.ptr, lex.end);
+
+ string rawStr(start, lex.ptr - start);
+ const int* keyVer = getKeyword(rawStr);
+ if (keyVer)
+ {
+ lex.last_token_bk = lex.last_token;
+ lex.line_start_bk = lex.line_start;
+ return *keyVer;
+ }
+ else
+ {
+ FBJSON::PathMethod* method = tokens->methods.get(rawStr);
+ if (method)
+ {
+ const TEXT* src = lex.ptr;
+ for (; lex.ptr < lex.end && isspace(*lex.ptr); lex.ptr++)
+ {
+ if (lex.ptr >= lex.end)
+ return -1;
+ }
+ if (*lex.ptr == '(')
+ {
+ yylval.methodType = *method;
+ lex.last_token_bk = lex.last_token;
+ lex.line_start_bk = lex.line_start;
+ return TOK_METHOD;
+ }
+ else
+ lex.ptr = src;
+ }
+ }
+
+ // Get the rest on allowed tokens for a string
+ lex.ptr = skipSpaces(lex.ptr, lex.end);
+ rawStr.assign(start, lex.ptr - start);
+
+
+ yylval.stringPtr = mkTempString(rawStr);
+ lex.last_token_bk = lex.last_token;
+ lex.line_start_bk = lex.line_start;
+ return TOK_STRING;
+ }
+
+ // Must be punctuation -- test for double character punctuation
+ if (lex.last_token + 1 < lex.end && !isspace(UCHAR(lex.last_token[1])))
+ {
+ const int keyVer = getNonCharKeyword(lex.last_token);
+ if (keyVer != 0)
+ {
+ ++lex.ptr;
+ return keyVer;
+ }
+ }
+
+ // Single character punctuation are simply passed on
+ return (UCHAR) c;
+}
+
+
+void PathParser::raiseFullErrorVector(const JsonStatusMsg& error, const YYPOSN& posn)
+{
+ string token(posn.firstPos);
+ if (token.length() > JSON_MAX_REPORT_SIZE)
+ {
+ token.resize(JSON_MAX_REPORT_SIZE);
+ token += "...";
+ }
+
+ if (posn.firstLine == 1)
+ {
+ json_syntax_exception::raise(
+ JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) <<
+ error <<
+ JsonStatusMsg(isc_jpath_problem_place) <<
+ JsonStatusMsgIntArg(FB_UINT64(posn.firstColumn)) <<
+ JsonStatusMsgStrArg(token));
+ }
+ else
+ {
+ json_syntax_exception::raise(
+ JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) <<
+ error <<
+ JsonStatusMsgIntArg(FB_UINT64((posn.firstColumn))) <<
+ JsonStatusMsgIntArg(FB_UINT64((posn.firstLine))) <<
+ JsonStatusMsgStrArg(token));
+ }
+}
+
+void PathParser::yyerror_detailed(const TEXT* /*error_string*/, int yychar, YYSTYPE&, YYPOSN& posn)
+{
+/**************************************
+ *
+ * y y e r r o r _ d e t a i l e d
+ *
+ **************************************
+ *
+ * Functional description
+ * Print a syntax error.
+ *
+ **************************************/
+
+ if (yychar < 1)
+ {
+ if (posn.firstLine == 1)
+ {
+ json_syntax_exception::raise(JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) <<
+ JsonStatusMsg(isc_jlexer_end) <<
+ JsonStatusMsgIntArg(FB_UINT64(posn.firstColumn)));
+ }
+ else
+ {
+ json_syntax_exception::raise(JsonStatusMsg(m_lexInFilter ? isc_jfilter_common : isc_jpath_common) <<
+ JsonStatusMsg(isc_jlexer_end_with_line) <<
+ JsonStatusMsgIntArg(FB_UINT64(posn.firstColumn)) <<
+ JsonStatusMsgIntArg(FB_UINT64(posn.firstLine)));
+ }
+ }
+ else
+ {
+ raiseFullErrorVector(JsonStatusMsg(isc_jlexer_invalid_syntax), posn);
+ }
+}
+
+
+void PathParser::checkPassing(const SmallString& passName) const
+{
+ if (m_passingKeys == nullptr)
+ {
+ json_syntax_exception::raise(JsonStatusMsg(isc_jpath_common) <<
+ JsonStatusMsg(isc_jlexer_passing_not_defined));
+ }
+ else if (!m_passingKeys->exist(passName))
+ {
+ json_syntax_exception::raise(JsonStatusMsg(isc_jpath_common) <<
+ JsonStatusMsg(isc_jlexer_passing_var_not_defined) <<
+ JsonStatusMsgStrArg(passName));
+ }
+}
diff --git a/src/jrd/json/path/JPathParser.h b/src/jrd/json/path/JPathParser.h
new file mode 100644
index 00000000000..e2d177ac9be
--- /dev/null
+++ b/src/jrd/json/path/JPathParser.h
@@ -0,0 +1,498 @@
+/*
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2023 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_PARSER_H
+#define JSON_PARSER_H
+
+#include "firebird.h"
+#include "../common/classes/fb_string.h"
+#include "../common/classes/PodOptional.h"
+
+#include "../jrd/json/JsonRuntime.h"
+#include "../jrd/json/path/JsonPath.h"
+
+namespace FBJSON {
+
+// Define before jparse.h
+struct JsonPathContext
+{
+ FBJSON::JsonPath* main = nullptr;
+ PathNode* current = nullptr;
+
+ PathVariable* variable = nullptr;
+ bool dropMain = false;
+
+ JsonPathContext()
+ { }
+
+ JsonPathContext(JsonPathContext&&) noexcept = delete;
+ JsonPathContext(const JsonPathContext&) = delete;
+
+ JsonPathContext& operator=(JsonPathContext&&) noexcept = delete;
+ JsonPathContext& operator=(const JsonPathContext&) = delete;
+
+ ~JsonPathContext()
+ {
+ if (dropMain)
+ delete main;
+ }
+
+ inline void addRange(const RangeSize up)
+ {
+ current->ranges.add().init(up);
+ }
+
+ inline void addRange(const RangeSize up, const RangeSize bot)
+ {
+ current->ranges.add().init(up, bot);
+ }
+
+ void clearRange()
+ {
+ current->ranges.clear();
+ }
+
+ inline JsonPath* extract() noexcept
+ {
+ dropMain = false;
+ return main;
+ }
+
+ inline bool isEmpty() const
+ {
+ return main->isZeroPath() && !current->filterNode.hasData();
+ }
+};
+
+} // namespace
+
+#include "../gen/jparse.h"
+
+using PassingKeys = Firebird::LeftPooledMap;
+
+namespace FBJSON {
+
+
+class PathParser : public Firebird::PermanentStorage
+{
+public:
+ PathParser(MemoryPool& pool, const std::string_view jsonPath);
+
+ PathParser(PathParser&&) noexcept = delete;
+ PathParser(const PathParser&) = delete;
+
+ PathParser& operator=(PathParser&&) noexcept = delete;
+ PathParser& operator=(const PathParser&) = delete;
+
+ ~PathParser();
+
+ JsonPathExpr* parse(const PassingKeys* keys = nullptr);
+
+ // Allow only one root expression for functions like JSON_EXISTS
+ inline void setMultipleRoots(const bool allow) noexcept
+ {
+ m_allowPathExtensions = allow;
+ }
+
+private:
+ // User-defined text position type.
+ struct Position
+ {
+ ULONG firstLine;
+ ULONG firstColumn;
+ ULONG lastLine;
+ ULONG lastColumn;
+ const char* firstPos;
+ const char* lastPos;
+ const char* leadingFirstPos;
+ const char* trailingLastPos;
+ };
+
+ typedef Position YYPOSN;
+ typedef int Yshort;
+
+ struct yyparsestate
+ {
+ yyparsestate* save; // Previously saved parser state
+ int state;
+ int errflag;
+ Yshort* ssp; // state stack pointer
+ YYSTYPE* vsp; // value stack pointer
+ YYPOSN* psp; // position stack pointer
+ YYSTYPE val; // value as returned by actions
+ YYPOSN pos; // position as returned by universal action
+ Yshort* ss; // state stack base
+ YYSTYPE* vs; // values stack base
+ YYPOSN* ps; // position stack base
+ int lexeme; // index of the conflict lexeme in the lexical queue
+ unsigned int stacksize; // current maximum stack size
+ Yshort ctry; // index in yyctable[] for this conflict
+ };
+
+ struct LexerState
+ {
+ // This is, in fact, parser state. Not used in lexer itself
+ int dsql_debug;
+
+ // Actual lexer state begins from here
+
+ const TEXT* leadingPtr;
+ const TEXT* ptr;
+ const TEXT* end;
+ const TEXT* last_token;
+ const TEXT* start;
+ const TEXT* line_start;
+ const TEXT* last_token_bk;
+ const TEXT* line_start_bk;
+ int prev_keyword;
+ };
+
+ [[maybe_unused]]
+ int yydebug = 0;
+
+ JsonPathContext* createContext()
+ {
+ JsonPathContext* context = FB_NEW_POOL(*m_tempPool) JsonPathContext();
+ context->main = FB_NEW_POOL(getPool()) JsonPath(getPool());
+ context->dropMain = true;
+ context->current = context->main->getRootNode();
+
+ return context;
+ }
+
+ JsonPathContext* createMainContext()
+ {
+ if (m_mainRootCreated)
+ {
+ if (!m_parseInFilter && !m_allowPathExtensions)
+ {
+ json_syntax_exception::raise(
+ JsonStatusMsg(isc_jpath_common) <<
+ JsonStatusMsg(isc_jpath_multiple_roots));
+ }
+
+ return createVariableContext(PathVariable::Type::JSON);
+ }
+ else
+ {
+ m_mainRootCreated = true;
+
+ // Main path
+ JsonPathContext* context = FB_NEW_POOL(*m_tempPool) JsonPathContext();
+ context->dropMain = false;
+ context->main = m_output->getJsonPath();
+ context->current = context->main->getRootNode();
+ context->variable = FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::ROOT);
+
+ return context;
+ }
+ }
+
+ JsonPathContext* createVariableContext(const PathVariable::Type type)
+ {
+ if (!m_parseInFilter)
+ {
+ if (type == PathVariable::Type::ITEM)
+ {
+ json_syntax_exception::raise(
+ JsonStatusMsg(isc_jpath_common) <<
+ JsonStatusMsg(isc_jpath_illegal_item_variable));
+ }
+
+ if (type == PathVariable::Type::PASSING)
+ {
+ m_mainRootCreated = true; // Allow passing instead of root path expression ($.)
+ }
+ }
+
+ PathVariable* var = FB_NEW_POOL(getPool()) PathVariable(getPool(), type);
+ return createVariableContext(*var);
+ }
+
+ JsonPathContext* createVariableContext(PathVariable& var)
+ {
+ JsonPathContext* context = FB_NEW_POOL(*m_tempPool) JsonPathContext();
+ var.path.reset(FB_NEW_POOL(getPool()) JsonPath(getPool()));
+ if (!m_isLaxMode)
+ var.path->setStrictFlag();
+
+ context->dropMain = false;
+ context->main = var.path.get();
+ context->current = var.path->getRootNode();
+ context->variable = &var;
+
+ return context;
+ }
+
+ JsonPathContext* createPassingContext(const SmallString& name)
+ {
+ checkPassing(name);
+ JsonPathContext* context = createVariableContext(PathVariable::Type::PASSING);
+ context->variable->name = name;
+ return context;
+ }
+
+ PathInjection& addPathInjection(JsonPathContext* main, const SmallString& name)
+ {
+ checkPassing(name);
+ PathVariable* passing = FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::PASSING);
+ passing->name = name;
+
+ return addPathInjection(main, FB_NEW_POOL(getPool()) JsonExprNode(getPool(), passing));
+ }
+
+ PathInjection& addPathInjection(JsonPathContext* context, JsonExprNode* node)
+ {
+ auto& injection = context->main->addInjection();
+ injection.expr = node;
+ injection.depth = context->current->depth + 1;
+
+ return injection;
+ }
+
+ void unwrapPrimary(JsonExprNode* nodeToUnwrap)
+ {
+ if (!nodeToUnwrap->isVariable())
+ return;
+
+ if (nodeToUnwrap->getVariable()->type == PathVariable::Type::ROOT)
+ {
+ m_output->getJsonPath()->unwrap();
+ }
+ else
+ nodeToUnwrap->unwrap();
+ }
+
+private:
+ Firebird::string* mkTempString(const Firebird::string& s)
+ {
+ return FB_NEW_POOL(*m_tempPool) Firebird::string(*m_tempPool, s);
+ }
+
+ Firebird::string* mkTempString(const char* s)
+ {
+ return FB_NEW_POOL(*m_tempPool) Firebird::string(*m_tempPool, s);
+ }
+
+ template
+ JsonExprNode* mkNode()
+ {
+ return JsonExprNode::make(getPool());
+ }
+
+ JsonExprNode* mkVariableNode(PathVariable* var)
+ {
+ return FB_NEW_POOL(getPool()) JsonExprNode(getPool(), var);
+ }
+
+ JsonExprNode* mkMethodNode(const PathMethod method, JsonExprNode* arg = nullptr)
+ {
+ auto* node = FB_NEW_POOL(getPool()) JsonExprNode(getPool(), method);
+
+ if (arg)
+ node->addChild(arg);
+
+ return node;
+ }
+
+ JsonExprNode* addOptTailNode(JsonExprNode* head, JsonExprNode* newTail)
+ {
+ if (newTail == nullptr)
+ return head;
+
+ bool needUnwrap = m_isLaxMode && JsonPath::isUnwrapMethod(newTail->getPathMethod());
+
+ // Inject head into expression
+ if (!head->canHasTail())
+ {
+ if (needUnwrap)
+ unwrapPrimary(head);
+
+ newTail->setHead(head);
+ return newTail;
+ }
+
+ // Add new tail
+ if (head->hasTail())
+ {
+ JsonExprNode* tailEnd = head->getTailNode();
+ addOptTailNode(tailEnd, newTail); // tail after tail
+ return head;
+ }
+
+ // Always inject root into expression
+ const bool isRootVariable = head->isVariable() && head->getVariable()->type == PathVariable::Type::ROOT;
+ if (isRootVariable)
+ {
+ if (needUnwrap)
+ unwrapPrimary(head);
+
+ newTail->setHead(head);
+ return newTail;
+ }
+
+ if (needUnwrap && head->testType(JsonExprNode::VARIABLE_NODE))
+ {
+ head->unwrap(); // Unwrap variable
+ needUnwrap = false;
+ }
+
+ if (!newTail->testType(JsonExprNode::FILTER_NODE))
+ newTail->makeHeadNode(needUnwrap);
+
+ // Add the next method as the second "tail" path
+ head->addChild(newTail);
+
+ return head;
+ }
+
+ void setPrimary(JsonExprNode* arithmetic)
+ {
+ if (!m_mainRootCreated)
+ {
+ json_syntax_exception::raise(
+ JsonStatusMsg(isc_jpath_common) <<
+ JsonStatusMsg(isc_jpath_missing_root));
+ }
+
+ fb_assert(m_output->getTail() == nullptr);
+ fb_assert(m_output->getMath() == nullptr);
+ m_output->resetExpr(m_pathRootTail.release(), arithmetic);
+ }
+
+ JsonExprNode* completePath(JsonExprNode* node)
+ {
+ JsonExprNode* rnode = node;
+ while (!rnode->isVariable()) // While method or a filter - get the source argument
+ {
+ if (rnode->getChildrenCount() == 0)
+ return node;
+
+ rnode = rnode->getFirstChild();
+ }
+
+ if (!m_pathRootTail.hasData() && rnode->getVariable()->type == PathVariable::Type::ROOT)
+ {
+ fb_assert(!node->testType(JsonExprNode::CALCULATION_NODE));
+ m_pathRootTail.reset(node);
+
+ // Return a dummy header node
+ return mkVariableNode(FB_NEW_POOL(getPool()) PathVariable(getPool(), PathVariable::Type::ROOT));
+ }
+ else
+ {
+ return node;
+ }
+ }
+
+ void yyReducePosn(YYPOSN& ret, YYPOSN* termPosns, YYSTYPE* termVals,
+ int termNo, int stkPos, int yychar, YYPOSN& yyposn, void*);
+
+ int yylex();
+ bool yylexSkipSpaces();
+ bool yylexSkipEol(); // returns true if EOL is detected and skipped
+ int yylexAux();
+
+ void yyerror_detailed(const TEXT* error_string, int yychar, YYSTYPE&, YYPOSN&);
+
+private:
+ void checkPassing(const SmallString& pass) const;
+ void raiseFullErrorVector(const JsonStatusMsg& error, const YYPOSN& posn);
+
+
+// start - defined in btyacc_json.ske
+private:
+ static void yySCopy(YYSTYPE* to, YYSTYPE* from, int size);
+ static void yyPCopy(YYPOSN* to, YYPOSN* from, int size);
+ static void yyFreeState(yyparsestate* p);
+
+ void yyMoreStack(yyparsestate* yyps);
+ yyparsestate* yyNewState(int size);
+
+
+private:
+ int parseAux();
+ int yylex1();
+ int yyexpand();
+// end - defined in btyacc_json.ske
+
+private:
+ USHORT parser_version;
+
+ Firebird::string transformedString;
+
+ // These value/posn are taken from the lexer
+ YYSTYPE yylval;
+ YYPOSN yyposn;
+
+ // These value/posn of the root non-terminal are returned to the caller
+ YYSTYPE yyretlval;
+ Position yyretposn;
+
+ int yynerrs;
+ int yym; // ASF: moved from local variable of Parser::parseAux()
+
+ // Current parser state
+ yyparsestate* yyps;
+ // yypath!=NULL: do the full parse, starting at *yypath parser state.
+ yyparsestate* yypath;
+ // Base of the lexical value queue
+ YYSTYPE* yylvals;
+ // Current posistion at lexical value queue
+ YYSTYPE* yylvp;
+ // End position of lexical value queue
+ YYSTYPE* yylve;
+ // The last allocated position at the lexical value queue
+ YYSTYPE* yylvlim;
+ // Base of the lexical position queue
+ Position* yylpsns;
+ // Current posistion at lexical position queue
+ Position* yylpp;
+ // End position of lexical position queue
+ Position* yylpe;
+ // The last allocated position at the lexical position queue
+ Position* yylplim;
+ // Current position at lexical token queue
+ Yshort* yylexp;
+ Yshort* yylexemes;
+
+ LexerState lex;
+
+private:
+ Firebird::AutoMemoryPool m_tempPool;
+ Firebird::AutoPtr m_output;
+ Firebird::AutoPtr m_pathRootTail;
+
+ const PassingKeys* m_passingKeys = nullptr;
+
+ SLONG m_filterBracketCounter = 0;
+ bool m_isLaxMode = true; // Global state
+ bool m_lexInFilter = false; // For error printing
+ bool m_parseInFilter = false; // For context check
+ bool m_mainRootCreated = false;
+
+ bool m_allowPathExtensions = true;
+};
+
+}
+
+#endif // DSQL_PARSER_H
diff --git a/src/jrd/json/path/JsonPath.cpp b/src/jrd/json/path/JsonPath.cpp
new file mode 100644
index 00000000000..dd77e89c84b
--- /dev/null
+++ b/src/jrd/json/path/JsonPath.cpp
@@ -0,0 +1,296 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonPath.cpp
+ * DESCRIPTION: JSON Path parser.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Denis Logashov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2017 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#include "JsonPath.h"
+#include "../JsonUtils.h"
+#include "../JsonConsts.h"
+#include "../JsonRuntime.h"
+
+#include "JPathParser.h"
+
+using namespace Firebird;
+using namespace FBJSON;
+
+JsonPath::JsonPath(MemoryPool& pool, const bool createPath)
+ : Firebird::PermanentStorage(pool),
+ m_injections(pool)
+{
+ if (createPath)
+ resetRootNode();
+}
+
+JsonPath::~JsonPath()
+{ }
+
+PathNode* JsonPath::resetRootNode()
+{
+ m_pathRootNode.reset(FB_NEW_POOL(getPool()) PathNode(getPool()));
+ return m_pathRootNode.get();
+}
+
+PathInjection& JsonPath::addInjection()
+{
+ return m_injections.add();
+}
+
+void JsonPath::unwrap()
+{
+ fb_assert(m_pathRootNode != nullptr);
+ PathNode* current = m_pathRootNode;
+ while (current->next)
+ {
+ current = current->next;
+ }
+ current->flags |= PathNode::FLAG_UNWRAP;
+}
+
+bool JsonPath::isZeroPath() const
+{
+ fb_assert(m_pathRootNode != nullptr);
+ return m_pathRootNode->next == nullptr;
+}
+
+bool JsonPath::isWrappingZeroPath() const
+{
+ fb_assert(m_pathRootNode != nullptr);
+ return m_pathRootNode->next != nullptr &&
+ m_pathRootNode->next->next == nullptr &&
+ m_pathRootNode->next->matchWrapPattern();
+}
+
+bool JsonPath::isEmpty(const UCHAR flagsToCheck) const
+{
+ return m_pathRootNode == nullptr || m_pathRootNode->isEmpty(flagsToCheck);
+}
+
+bool JsonPath::isAggregateMethod(const PathMethod method)
+{
+ switch (method)
+ {
+ case PathMethod::SIZE:
+ case PathMethod::TYPE:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool JsonPath::isUnwrapMethod(const PathMethod method)
+{
+ switch (method)
+ {
+ case PathMethod::NONE:
+ case PathMethod::SIZE:
+ case PathMethod::TYPE:
+ return false;
+ default:
+ return true;
+ }
+}
+
+
+JsonPathExpr::JsonPathExpr(MemoryPool& pool) :
+ Firebird::PermanentStorage(pool),
+ m_jpath(FB_NEW_POOL(pool) JsonPath(pool))
+{ }
+
+JsonPathExpr::~JsonPathExpr()
+{ }
+
+void JsonPathExpr::resetExpr(JsonExprNode* tail, JsonExprNode* arithmetic)
+{
+ // Omit empty arithmetic path (it contains only a arithmetic variable)
+ // tail is null when the input is a passing variable. Example: json_value('"test"', '$passed_timestamp')
+ if (tail != nullptr && ((tail->isVariable() && !tail->hasTail()) || tail->isRootEmpty()))
+ {
+ delete tail;
+ tail = nullptr;
+ }
+ m_tail = tail;
+
+ fb_assert(arithmetic);
+ if ((arithmetic->isVariable() && arithmetic->getVariable()->type == PathVariable::Type::ROOT) || arithmetic->isRootEmpty())
+ {
+ delete arithmetic;
+ arithmetic = nullptr;
+ }
+ m_math = arithmetic;
+}
+
+const JsonExprNode* JsonPathExpr::getMath() const
+{
+ return m_math;
+}
+
+const JsonExprNode* JsonPathExpr::getTail() const
+{
+ return m_tail;
+}
+
+// PathNode
+
+PathNode::PathNode(MemoryPool& pool, PathNode* prev)
+ : Firebird::PermanentStorage(pool),
+ field(pool), ranges(pool), prev(prev)
+{
+ if (prev)
+ {
+ fb_assert(prev->next == nullptr);
+
+ prev->next = this;
+ depth = prev->depth + 1;
+ }
+}
+
+PathNode::~PathNode()
+{
+ PathNode* cur = next;
+ PathNode* nnext;
+ while (cur)
+ {
+ nnext = cur->next;
+ cur->next = nullptr;
+ delete cur;
+ cur = nnext;
+ }
+}
+
+PathNode* PathNode::update()
+{
+ if (isTemporary())
+ {
+ PathNode* newCurrent = next ? next : prev;
+
+ fb_assert(prev);
+ prev->filterNode.reset(filterNode.release());
+ removeThis();
+ return newCurrent;
+ }
+ else
+ enable();
+
+ return this;
+}
+
+PathNode* PathNode::insertUnwrapNode()
+{
+ insert();
+ next->state = PathNodeState::TEMPORARY;
+ next->matched = false;
+ next->filterNode = std::move(filterNode);
+ next->selectAll();
+
+ return next;
+}
+
+void PathNode::removeThis() noexcept
+{
+ if (next)
+ {
+ next->prev = prev;
+ PathNode* temp = next;
+ while (temp)
+ {
+ --temp->depth;
+ temp = temp->next;
+ }
+ }
+
+ if (prev)
+ {
+ if (isTemporary())
+ prev->filterNode = std::move(filterNode);
+
+ prev->next = next;
+ }
+
+ next = nullptr;
+ prev = nullptr;
+ delete this;
+}
+
+bool PathNode::equalsNoComplex(const JsonLevelNode* const jsonLevel)
+{
+ // Previous level should be matched
+ PathNode* enabledPrev = getEnabledPrev();
+ if (enabledPrev == nullptr || !enabledPrev->matched || jsonLevel->depth > depth)
+ return false;
+
+ matched = false;
+ switch (getSuperType(type, jsonLevel->itemType))
+ {
+ case getSuperType(ItemType::FIELD, ItemType::FIELD):
+ if (!matchFieldName(jsonLevel->field))
+ return false;
+
+ // Check the previous state because the current one can be skipped (when an unwrapping node has been inserted)
+ fb_assert(enabledPrev->filterNode == nullptr);
+ matched = true;
+ break;
+ case getSuperType(ItemType::FIELD, ItemType::ARRAY_ELEMENT):
+ if (enabledPrev->matchUnwrapPattern())
+ {
+ // $.field
+ // [{"filed":42}]
+ // Expected object but encoutred an array
+
+ // Modify path
+ // $.field => $[*].field
+ enabledPrev->insertUnwrapNode();
+ enabledPrev->next->matched = true;
+ }
+ return false;
+ case getSuperType(ItemType::ARRAY_ELEMENT, ItemType::ARRAY_ELEMENT):
+ if (!isInSimpleRange(jsonLevel->indexInArray))
+ return false;
+
+ fb_assert(enabledPrev->filterNode == nullptr);
+ matched = true;
+ break;
+ case getSuperType(ItemType::ARRAY_ELEMENT, ItemType::FIELD):
+ if (next && matchWrapPattern())
+ {
+ fb_assert(!next->isDisabled());
+
+ // $[0].a
+ // {"a":42}
+ matched = true;
+ return next->equalsNoComplex(jsonLevel);
+ }
+ return false;
+
+ default:
+ fb_assert(false);
+ }
+
+ return isNextEmpty() && jsonLevel->depth == depth; // reached end
+}
+
+
+bool PathVariable::hasPath(const UCHAR flagsMask) const
+{
+ return path.hasData() && !path->isEmpty(flagsMask);
+}
diff --git a/src/jrd/json/path/JsonPath.h b/src/jrd/json/path/JsonPath.h
new file mode 100644
index 00000000000..3c1da52303f
--- /dev/null
+++ b/src/jrd/json/path/JsonPath.h
@@ -0,0 +1,699 @@
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonPath.h
+ * DESCRIPTION: JSON Path parser.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Denis Logashov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2017 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#ifndef JSON_PATH_H
+#define JSON_PATH_H
+
+#include "../classes/JsonTypes.h"
+
+#include "../common/classes/init.h"
+
+namespace FBJSON {
+
+inline constexpr PathArrayIndex JPATH_ARRAY_LAST_INDEX = -1;
+
+
+struct PathNode;
+class JsonExprNode;
+class JsonPath;
+class PathParser;
+class PassingList;
+struct PathVariable;
+struct PathInjection;
+
+
+class JsonPath : Firebird::PermanentStorage
+{
+ enum Flags : UCHAR
+ {
+ FLAG_STRICT = 1, // lax by default
+ FLAG_HAS_INDEX = 2,
+ FLAG_HAS_FIELD = 4,
+ FLAG_COMPLEX_RANGE = 8,
+ };
+
+public:
+ struct Range
+ {
+ public:
+ PathArrayIndex down = 0;
+ PathArrayIndex up = 0;
+
+ constexpr void init(const PathArrayIndex singleIndex = 0) noexcept
+ {
+ down = singleIndex;
+ up = singleIndex;
+ }
+
+ constexpr void init(const PathArrayIndex down, const PathArrayIndex up) noexcept
+ {
+ this->down = down;
+ this->up = up;
+ }
+
+ // Support negative indexes
+ // Return signed because the index can be negative even after array size adding
+ constexpr JsonArrayIndex getPreparedUp(const ArraySize size) const noexcept
+ {
+ return up >= 0 ? up : PathArrayIndex(size) + up;
+ }
+
+ // Return signed because the index can be negative even after array size adding
+ constexpr JsonArrayIndex getPreparedDown(const ArraySize size) const noexcept
+ {
+ return down >= 0 ? down : down + static_cast(size);
+ }
+
+ // Simple range
+ constexpr bool enters(const PathArrayIndex index) const
+ {
+ fb_assert(!isComplex());
+ fb_assert(down >= 0);
+ fb_assert(up >= JPATH_ARRAY_LAST_INDEX);
+ return index >= down && (up == JPATH_ARRAY_LAST_INDEX || index <= up);
+ }
+
+ // When a range can get first element, it is possible a wrapper
+ // Example: the JSON '{"data":42}' with 'lex $[0].a' is equals to [{"data":42}]
+ constexpr bool canWrap() const noexcept
+ {
+ // Only -1 is a valid negative value so array size does not matter here
+ return down == 0 || up == JPATH_ARRAY_LAST_INDEX || up == 0;
+ }
+
+ inline bool canGetFirst(const ArraySize arraySize) const noexcept
+ {
+ return down == 0 || getPreparedUp(arraySize) == 0;
+ }
+
+ constexpr bool isComplex() const noexcept
+ {
+ // Allow wildcard (*), simple ranges (1 to 2, 0 to last) and non negative indexes (2, 39, 0)
+ // `last` token and all the negative values are consider as complex
+ if (isWildcard())
+ return false;
+
+ return down < 0 || up < 0;
+ }
+
+ constexpr bool isWildcard() const noexcept
+ {
+ return down == 0 && up == JPATH_ARRAY_LAST_INDEX;
+ }
+ };
+
+ JsonPath(MemoryPool& pool, const bool createPath = true);
+
+ JsonPath(JsonPath&&) noexcept = delete;
+ JsonPath(const JsonPath&) = delete;
+
+ JsonPath& operator=(JsonPath&&) noexcept = delete;
+ JsonPath& operator=(const JsonPath&) = delete;
+
+ ~JsonPath();
+
+ void unwrap();
+
+ PathNode* resetRootNode();
+ PathInjection& addInjection();
+
+ const PathNode* getRootNode() const noexcept
+ {
+ return m_pathRootNode.get();
+ }
+
+ PathNode* getRootNode() noexcept
+ {
+ return m_pathRootNode.get();
+ }
+
+public:
+ inline bool hasFields() const noexcept
+ {
+ return flags & FLAG_HAS_FIELD;
+ }
+
+ inline bool hasIndexes() const noexcept
+ {
+ return flags & FLAG_HAS_INDEX;
+ }
+
+ inline bool hasComplexRange() const noexcept
+ {
+ return flags & FLAG_COMPLEX_RANGE;
+ }
+
+ inline bool isLax() const noexcept
+ {
+ return (flags & FLAG_STRICT) == 0;
+ }
+
+ inline bool isStrict() const noexcept
+ {
+ return flags & FLAG_STRICT;
+ }
+
+ bool isZeroPath() const;
+ bool isWrappingZeroPath() const;
+ bool isEmpty(const UCHAR flagsToCheck) const;
+
+ inline void setIndexesFlag()
+ {
+ flags |= FLAG_HAS_INDEX;
+ }
+
+ inline void setComplexRangeFlag()
+ {
+ flags |= FLAG_COMPLEX_RANGE;
+ }
+
+ inline void setFieldsFlag()
+ {
+ flags |= FLAG_HAS_FIELD;
+ }
+
+ inline void setStrictFlag()
+ {
+ flags |= FLAG_STRICT;
+ }
+
+public:
+ static bool isAggregateMethod(const PathMethod method);
+ static bool isUnwrapMethod(const PathMethod method);
+
+protected:
+ UCHAR flags = 0;
+
+private:
+ Firebird::AutoPtr m_pathRootNode;
+
+ // Instead of a field or an index, an expression can be used.
+ // But in 99% of cases, this is not the case. So use simple scalars in the PathNode's and
+ // inject calculated value if needed before the processing
+ Firebird::ObjectsArray m_injections;
+};
+
+
+// Store parsed JSON Path in a 3 components
+//
+// is the main root path.
+// are the nodes used to modify output value of its path
+// are the nodes used to apply arithmetical expressions to the node
+// $.value.keyvalue().value + $.mod + 4
+// <-----> <--------------> <--------->
+// primary tail arithmetic
+
+// In case of sequence, arithmetic applies to each sequence element
+
+class JsonPathExpr : public Firebird::PermanentStorage
+{
+public:
+ JsonPathExpr(MemoryPool& pool);
+
+ JsonPathExpr(JsonPathExpr&&) noexcept = delete;
+ JsonPathExpr(const JsonPathExpr&) = delete;
+
+ JsonPathExpr& operator=(JsonPathExpr&&) noexcept = delete;
+ JsonPathExpr& operator=(const JsonPathExpr&) = delete;
+
+ ~JsonPathExpr();
+
+ const JsonExprNode* getMath() const;
+ const JsonExprNode* getTail() const;
+ inline const JsonPath* getJsonPath() const noexcept
+ {
+ return m_jpath.get();
+ }
+
+ inline JsonPath* getJsonPath() noexcept
+ {
+ return m_jpath.get();
+ }
+
+ // arithmetic(.)
+ void resetExpr(JsonExprNode* tail, JsonExprNode* arithmetic);
+ inline bool hasMath() const
+ {
+ return getMath() != nullptr;
+ }
+
+private:
+ Firebird::AutoPtr m_jpath;
+ Firebird::AutoPtr m_tail; // Accepts to each element
+ Firebird::AutoPtr m_math; // Accepts to output expression
+};
+
+using RangesArray = Firebird::HalfStaticArray;
+
+
+enum class PathNodeState : UCHAR
+{
+ NORMAL = 0,
+ TEMPORARY,
+ DISABLED
+};
+
+// Each part of a json path will be converted into a PathNode.
+// We could use just an array but for lax mode it may be necessary to insert a node
+// For example:
+// ($.price.value) => node -> node('price') -> node('value');
+// ($[0].value) => node(ranges:[0]) -> node('value');
+// When price is an object the path wont change but for an array we will insert an extra node:
+// node('price')->node('value') ==> node('price') -> node([*]) -> node('value')
+struct PathNode : Firebird::PermanentStorage
+{
+public:
+ PathNode(MemoryPool& pool, PathNode* prev = nullptr);
+
+ PathNode(PathNode&&) noexcept = delete;
+ PathNode(const PathNode&) = delete;
+
+ PathNode& operator=(PathNode&&) = delete;
+ PathNode& operator=(const PathNode&) = delete;
+ ~PathNode();
+
+public: // Node modification methods
+ inline void enable() noexcept
+ {
+ if (state != PathNodeState::DISABLED)
+ return;
+
+ state = PathNodeState::NORMAL;
+ if (next == nullptr)
+ return;
+
+ PathNode* it = next;
+ it->prev = this;
+ while (it)
+ {
+ ++it->depth;
+ it = it->next;
+ }
+ }
+
+ inline void disable() noexcept
+ {
+ if (isDisabled())
+ return;
+
+ state = PathNodeState::DISABLED;
+
+ if (next == nullptr)
+ return;
+
+ PathNode* it = next;
+ next->prev = prev;
+
+ while (it)
+ {
+ --it->depth;
+ it = it->next;
+ }
+ }
+
+ inline void selectAll()
+ {
+ type = ItemType::ARRAY_ELEMENT;
+ ranges.add().init(0, JPATH_ARRAY_LAST_INDEX);
+ }
+
+ PathNode* update();
+
+ inline void updateLaxPatterns()
+ {
+ for (FB_SIZE_T i = 0; i < ranges.getCount(); i++)
+ {
+ if (ranges[i].canWrap())
+ {
+ flags |= PathNode::FLAG_WRAP;
+ break;
+ }
+ }
+ }
+
+ inline PathNode* add()
+ {
+ fb_assert(next == nullptr);
+ return next = FB_NEW_POOL(getPool()) PathNode(getPool(), this);
+ }
+
+ PathNode* insertUnwrapNode();
+
+ inline PathNode* insert()
+ {
+ if (next == nullptr)
+ return add();
+ else
+ {
+ PathNode* oldNext = next;
+ // this<-->oldNext
+ next = nullptr;
+ PathNode* newNext = FB_NEW_POOL(getPool()) PathNode(getPool(), this);
+ newNext->next = oldNext;
+ oldNext->prev = newNext;
+ // this<-->newNext<-->oldNext
+
+ while (oldNext)
+ {
+ ++oldNext->depth;
+ oldNext = oldNext->next;
+ }
+
+ next = newNext;
+ return newNext;
+ }
+ }
+
+ void removeThis() noexcept;
+
+ inline PathNode* moveTo(const SSHORT newDepth) noexcept
+ {
+ if (depth == newDepth)
+ {
+ if (isDisabled())
+ {
+ return prev;
+ }
+ else
+ return this;
+ }
+ else if (depth > newDepth)
+ {
+ // Move Down
+
+ PathNode* temp = this;
+ while (temp && temp->depth > newDepth)
+ {
+ temp->matched = false;
+ temp = temp->prev;
+ }
+
+ if (temp && temp->state != PathNodeState::TEMPORARY)
+ temp->matched = false;
+
+ return temp;
+ }
+ else
+ {
+ // Move Up
+
+ // Don't check for update because the state adds in a previous level to the next one
+ PathNode* temp = this;
+ while (temp && temp->depth < newDepth)
+ {
+ temp = temp->next;
+ }
+
+ while (temp && temp->isDisabled())
+ {
+ temp = temp->next;
+ }
+
+ return temp;
+ }
+ }
+
+public: // Checkers
+ // Check the range to match the array size
+ // Throw an error on fail
+ void validateRange(const ArraySize arraySize) const
+ {
+ for (USHORT i = 0; i < ranges.getCount(); ++i)
+ {
+ // Current jsonLevel is an element level. Get size from parent (array) level
+ const PathArrayIndex rangeA = ranges[i].getPreparedDown(arraySize);
+ const PathArrayIndex rangeB = ranges[i].getPreparedUp(arraySize);
+
+ // [4,2] for example
+ if (rangeA > rangeB)
+ {
+ json_strict_exception::raise(JsonStatusMsg(isc_jstrict_common) <<
+ JsonStatusMsg(isc_jstrict_invalid_range) << SINT64(rangeA) << SINT64(rangeB));
+ }
+
+ // [-20,-10] for example
+ if (rangeA < 0 || rangeA >= PathArrayIndex(arraySize) || rangeB >= PathArrayIndex(arraySize))
+ {
+ const SINT64 printSize = arraySize > 0 ? (arraySize - 1) : 0;
+
+ json_strict_exception::raise(JsonStatusMsg(isc_jstrict_common) <<
+ JsonStatusMsg(isc_jstrict_out_of_range) <<
+ SINT64(rangeA) << SINT64(rangeB) <field == Tokens::ANY_FIELD)
+ return true;
+
+ if (this->field == name)
+ return true;
+
+ return false;
+ }
+
+ inline bool isInSimpleRange(const JsonArrayIndex index) const
+ {
+ if (ranges.isEmpty())
+ return false;
+
+ for (ULONG i = 0; i < ranges.getCount(); i++)
+ {
+ if (ranges[i].enters(index))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ inline bool isNextEmpty() const noexcept
+ {
+ // Two diabled in a row is impossible but check it anyway
+ return next == nullptr || (next->isDisabled() && next->isNextEmpty());
+ }
+
+ // No complex ranges should be in the node
+ // Complex ranges should be checked via JSON BINARY to avoid extra parsing to calculate array size
+ bool equalsNoComplex(const JsonLevelNode* const jsonLevel);
+
+ bool equalsZeroDepth(const JsonLevelNode* jsonLevel) const noexcept
+ {
+ return depth == 0 && jsonLevel->depth == 0;
+ }
+
+ bool canWrap(const JsonLevelNode* jsonLevel) const noexcept
+ {
+ // We do not add (wrap) json level, instead modify the path.
+ // Example: for '{"data":{"a":1}}' we can convert '$.data[0].a to $.data.a
+ if (isNextEmpty())
+ return false;
+
+ return matched && next->matchWrapPattern() && jsonLevel->depth == depth;
+ }
+
+ inline bool canWrapEnd(const JsonLevelNode* jsonLevel) const
+ {
+ // We do not add (wrap) json level, instead modify the path.
+ // Example: for '{"data":1}' we can convert '$.data[0].a to $.data
+ return canWrap(jsonLevel) && next->isNextEmpty();
+ }
+
+ inline bool canUnwrapEnd() noexcept
+ {
+ return matched && next == nullptr && matchUnwrapPattern();
+ }
+
+ inline bool canUnwrapMiddle(const JsonLevelNode* jsonLevel) const noexcept
+ {
+ // Can insert [*] to path
+ return matched && next && jsonLevel->isArray() && matchUnwrapPattern();
+ }
+
+ inline bool isTemporary() const noexcept
+ {
+ return state == PathNodeState::TEMPORARY;
+ }
+
+ inline bool isDisabled() const noexcept
+ {
+ return state == PathNodeState::DISABLED;
+ }
+
+ inline bool isEmpty(const USHORT flagsToCheck) const
+ {
+ return next == nullptr && ((flags & flagsToCheck) == 0) && !filterNode.hasData();
+ }
+
+ inline bool matchWrapPattern() const noexcept
+ {
+ return flags & FLAG_WRAP;
+ }
+
+ inline bool matchUnwrapPattern() const noexcept
+ {
+ return flags & FLAG_UNWRAP;
+ }
+
+public: // Getters
+ inline PathNode* getEnabledPrev() const
+ {
+ return (prev && prev->isDisabled() ? prev->getEnabledPrev() : prev);
+ }
+
+private:
+ friend class JsonPath;
+
+public:
+ enum LexPattern
+ {
+ FLAG_NONE = 0,
+ FLAG_WRAP = 1,
+ FLAG_UNWRAP = 2,
+ FLAG_PROCESSED = 4,
+ FLAG_ALL_LEX = FLAG_WRAP | FLAG_UNWRAP
+ };
+
+ // Common data
+ ItemType type = ItemType::FIELD;
+ SSHORT depth = 0;
+ UCHAR flags = 0;
+ Firebird::AutoPtr filterNode;
+
+ // Filed
+ SmallString field;
+
+ // Range
+ RangesArray ranges;
+
+ // Common
+ PathNode* next = nullptr;
+ PathNode* prev = nullptr;
+
+ // Mutable fields
+ PathNodeState state = PathNodeState::NORMAL;
+ bool matched = false;
+};
+
+
+struct PathVariable : private Firebird::PermanentStorage
+{
+ enum class Type
+ {
+ JSON = 0, // The source json
+ ITEM, // Root in a filter
+ PASSING,
+ ROOT, // Calculated primary root variable to pass to the path tail expr
+ HEAD
+ };
+
+ SmallString name;
+ Firebird::AutoPtr path;
+ Type type = Type::ITEM;
+
+ PathVariable(MemoryPool& pool, const Type type = Type::ITEM)
+ : Firebird::PermanentStorage(pool),
+ name(pool),
+ type(type)
+ { }
+
+ PathVariable(PathVariable&&) noexcept = delete;
+ PathVariable(const PathVariable&) = delete;
+
+ PathVariable& operator=(PathVariable&&) noexcept = delete;
+ PathVariable& operator=(const PathVariable&) = delete;
+
+ ~PathVariable() = default;
+
+ inline bool isItem() const noexcept
+ {
+ return type == Type::ITEM;
+ }
+
+ inline bool isPassing() const noexcept
+ {
+ return type == Type::PASSING;
+ }
+
+ bool hasPath(const UCHAR flagsMask) const;
+};
+
+
+// Use it to calculate and inject result into JSON Path
+struct PathInjection : private Firebird::PermanentStorage
+{
+ enum class Position
+ {
+ FIELD = 0,
+ RANGE_UP,
+ RANGE_DOWN,
+ RANGE_INDEX
+ };
+
+ // Where to inject
+ Position position = Position::FIELD;
+ SSHORT rangeIndex = -1;
+ SSHORT depth = -1;
+
+ // What to inject
+ Firebird::AutoPtr expr;
+
+ PathInjection(MemoryPool& pool)
+ : Firebird::PermanentStorage(pool)
+ { }
+
+ PathInjection(PathInjection&&) noexcept = delete;
+ PathInjection(const PathInjection&) = delete;
+
+ PathInjection& operator=(PathInjection&&) noexcept = delete;
+ PathInjection& operator=(const PathInjection&) = delete;
+
+ ~PathInjection() = default;
+};
+
+}
+
+#endif // JSON_PATH_H
diff --git a/src/jrd/json/path/JsonPathParser.y b/src/jrd/json/path/JsonPathParser.y
new file mode 100644
index 00000000000..b23b4b4fc19
--- /dev/null
+++ b/src/jrd/json/path/JsonPathParser.y
@@ -0,0 +1,800 @@
+%{
+/*
+ * PROGRAM: Firebird JSON logic.
+ * MODULE: JsonPathParser.y
+ * DESCRIPTION: JSON Path parser.
+ *
+ * The contents of this file are subject to the Initial
+ * Developer's Public License Version 1.0 (the "License");
+ * you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ * http://www.ibphoenix.com/main.nfs?a=ibphoenix&page=ibp_idpl.
+ *
+ * Software distributed under the License is distributed AS IS,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied.
+ * See the License for the specific language governing rights
+ * and limitations under the License.
+ *
+ * The Original Code was created by Artyom Abakumov
+ * for Red Soft Corporation.
+ *
+ * Copyright (c) 2023 Red Soft Corporation
+ * and all contributors signed below.
+ *
+ * All Rights Reserved.
+ * Contributor(s): ______________________________________.
+ */
+
+#include "firebird.h"
+#include "../jrd/json/JsonRuntime.h"
+#include "../jrd/json/path/JsonPath.h"
+#include "../jrd/json/JsonConsts.h"
+
+#define YYREDUCEPOSNFUNC yyReducePosn
+#define YYREDUCEPOSNFUNCARG NULL
+
+
+// ASF: Inherited attributes (aka rule parameters) are executed even when in trial mode, but action
+// rules ({}) are executed only when in full parse mode. NOTRIAL should be used to avoid segfaults
+// due to accessing invalid pointers in parameters (not yet returned from action rules).
+#define NOTRIAL(x) (yytrial ? NULL : (x))
+
+
+#include "../dsql/chars.h"
+
+using namespace Firebird;
+using namespace FBJSON;
+
+%}
+
+
+// token declarations
+
+%token INTEGER16
+
+%token