diff --git a/crates/rupg-analyze/src/node.rs b/crates/rupg-analyze/src/node.rs index 01a0c9c4..1499735b 100644 --- a/crates/rupg-analyze/src/node.rs +++ b/crates/rupg-analyze/src/node.rs @@ -440,6 +440,13 @@ fn write_value(value: &Value, out: &mut String) { hex(&bytes, out); out.push(')'); } + Value::JsonPath(path) => { + let mut bytes = Vec::new(); + rupg_types::jsonpath_send(path, &mut bytes); + out.push_str("(jsonpath "); + hex(&bytes, out); + out.push(')'); + } Value::Range(range) => write_range(range, out), Value::Multirange(ranges) => { out.push_str("(multirange"); @@ -807,6 +814,13 @@ fn value_of(item: &Item) -> Result { Value::TsQuery(value) } } + "jsonpath" => { + let bytes = bytes_of(part(0)?)?; + // The input checked the patterns of `like_regex` when it made the value. + let value = rupg_types::jsonpath_recv(&mut Recv::new(&bytes), &|_, _| Ok(())) + .map_err(|_| bad("the bytes of a jsonpath"))?; + Value::JsonPath(Box::new(value)) + } "oid" => Value::Oid(number(part(0)?)?), "char" => Value::Char(number(part(0)?)?), "text" => match parts.first() { @@ -956,6 +970,17 @@ mod tests { ), oid::TSQUERY, ), + ( + Value::JsonPath(Box::new( + rupg_types::jsonpath_parse( + "strict $.a[1 to last] ? (@ like_regex \"^b\" flag \"i\").**{2}", + false, + &|_, _| Ok(()), + ) + .expect("jsonpath"), + )), + oid::JSONPATH, + ), ( Value::Multirange(vec![ Range { diff --git a/crates/rupg-func/src/io.rs b/crates/rupg-func/src/io.rs index 2e9f435c..3a637620 100644 --- a/crates/rupg-func/src/io.rs +++ b/crates/rupg-func/src/io.rs @@ -112,6 +112,7 @@ pub fn output_supported(ty: u32) -> bool { | oid::TXID_SNAPSHOT | oid::TSVECTOR | oid::TSQUERY + | oid::JSONPATH ) || RegKind::from_oid(ty).is_some() || is_row_type(ty) } @@ -195,6 +196,10 @@ pub fn output(ty: u32, value: &Value, session: &dyn Session, out: &mut Vec) types::tsquery_deparse(items, out); return Ok(()); } + if let Value::JsonPath(path) = value { + types::jsonpath_deparse(path, out); + return Ok(()); + } if let (Some(kind), Value::Bytea(v)) = (types::GeoKind::from_oid(ty), value) { types::geo_out(kind, v, session.extra_float_digits(), out); return Ok(()); @@ -498,6 +503,19 @@ fn tsquery_in( Ok(items) } +/// `jsonpath_in`. A number that is too large and a path that is too deep are errors that `pg_input_is_valid` does not catch. The error for a code point that is too large is not the same in the soft input, as in PostgreSQL. +fn jsonpath_in(text: &str, noisy: bool) -> std::result::Result { + types::jsonpath_parse(text, !noisy, &crate::regex::validate).map_err(|error| { + if error.sqlstate == SqlState::NUMERIC_VALUE_OUT_OF_RANGE + || error.sqlstate == SqlState::STATEMENT_TOO_COMPLEX + { + Failed::Hard(type_error(error)) + } else { + Failed::from(error) + } + }) +} + /// The text input function of the type. `noisy` is false for the soft input of `pg_input_is_valid`, which gives no notices, as `escontext` does in PostgreSQL. fn input_value( ty: u32, @@ -555,6 +573,7 @@ fn input_value( oid::INT4 => Value::Int4(types::int4_in(text)?), oid::TSVECTOR => Value::TsVector(types::tsvector_parse(text)?), oid::TSQUERY => Value::TsQuery(tsquery_in(text, session, noisy)?), + oid::JSONPATH => Value::JsonPath(Box::new(jsonpath_in(text, noisy)?)), oid::INT8 => Value::Int8(types::int8_in(text)?), oid::OID => Value::Oid(types::oid_in(text)?), oid::MONEY => Value::Int8(types::cash_in(text)?), @@ -815,6 +834,9 @@ fn receive_from(ty: u32, recv: &mut Recv<'_>, typmod: i32) -> std::result::Resul oid::OID => Value::Oid(recv.u32()?), oid::TSVECTOR => Value::TsVector(types::tsvector_recv(recv)?), oid::TSQUERY => Value::TsQuery(types::tsquery_recv(recv)?), + oid::JSONPATH => { + Value::JsonPath(Box::new(types::jsonpath_recv(recv, &crate::regex::validate)?)) + } ty if RegKind::from_oid(ty).is_some() => Value::Oid(recv.u32()?), oid::JSON => Value::text(types::json_in(recv.text()?)?), oid::JSONB => Value::Text(types::jsonb_recv(recv.rest())?), @@ -926,6 +948,7 @@ pub fn send(ty: u32, value: &Value, out: &mut Vec) -> Result<()> { (_, Value::Multirange(ranges)) => multirange_send(ty, ranges, out)?, (_, Value::TsVector(lexemes)) => types::tsvector_send(lexemes, out), (_, Value::TsQuery(items)) => types::tsquery_send(items, out), + (_, Value::JsonPath(path)) => types::jsonpath_send(path, out), (_, Value::Null) => return Err(Error::internal("the send function got a null value")), _ => return Err(not_yet(format!("binary output of type {}", type_name(ty)))), } diff --git a/crates/rupg-func/src/regex/mod.rs b/crates/rupg-func/src/regex/mod.rs index a613af0f..4a55040f 100644 --- a/crates/rupg-func/src/regex/mod.rs +++ b/crates/rupg-func/src/regex/mod.rs @@ -195,6 +195,11 @@ pub fn auth_compile(pattern: &str) -> std::result::Result<(), &'static str> { Regex::compile(pattern, flags::ADVANCED).map(drop).map_err(Code::message) } +/// The check of the pattern of `like_regex` in a `jsonpath`: the message of `pg_regerror` when `pattern` is not valid with the `cflags`. +pub(crate) fn validate(pattern: &str, cflags: u32) -> std::result::Result<(), String> { + Regex::compile(pattern, cflags).map(drop).map_err(|code| code.message().to_string()) +} + /// `regexec_auth_token`: find the regular expression `pattern` of [`auth_compile`] in `s`. The result is `None` when the pattern does not match. /// /// # Errors diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index c16de374..8f966454 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -1874,6 +1874,12 @@ mod tests { big_stack(|| script(include_str!("connection/tsquery.test"))); } + /// The type `jsonpath`: the text input with its keys, variables, literals, operators, filters, subscripts and methods, the escapes and the comments, the errors of the scanner and of the parser at the same token as PostgreSQL, the checks of `@` and `last`, the output, arrays of `jsonpath`, `pg_input_is_valid`, `pg_input_error_info`, the text and binary forms of `COPY` and a query with parameters. `connection/jsonpath.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn jsonpath() { + big_stack(|| script(include_str!("connection/jsonpath.test"))); + } + /// The operators `@@` and `@@@` of `tsvector` and `tsquery`: the operators `&`, `|` and `!`, weights, prefixes, a vector with no positions, `<->` and `` with `!`, `&` and `|` under them, the largest distance, the bits of a position that PostgreSQL keeps, `GROUP BY` and a query with parameters. `connection/ts_match.test` is the output of PostgreSQL 19 for the same script in a new database. #[test] fn ts_match() { diff --git a/crates/rupg-session/src/connection/jsonpath.test b/crates/rupg-session/src/connection/jsonpath.test new file mode 100644 index 00000000..68fa9bcf --- /dev/null +++ b/crates/rupg-session/src/connection/jsonpath.test @@ -0,0 +1,490 @@ +> SELECT '$'::jsonpath, 'strict $'::jsonpath, 'lax $'::jsonpath, '$.a'::jsonpath, '$.a.b'::jsonpath, '$."a b"'::jsonpath, '$.a[*]'::jsonpath, '$.*'::jsonpath, '$.**'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $,strict $,$,$."a",$."a"."b",$."a b",$."a"[*],$.*,$.** +SELECT 1 +> SELECT '1 + 2'::jsonpath, '1 + 2 + 3'::jsonpath, '1 + (2 + 3)'::jsonpath, '1 * 2 + 3'::jsonpath, '1 + 2 * 3'::jsonpath, '-1'::jsonpath, '- -1'::jsonpath, '-$.a'::jsonpath, '+$.a'::jsonpath, '-(1 + 2)'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row (1 + 2),((1 + 2) + 3),(1 + (2 + 3)),(1 * 2 + 3),(1 + 2 * 3),-1,1,(-$."a"),(+$."a"),(-(1 + 2)) +SELECT 1 +> SELECT '$ ? (@ > 1)'::jsonpath, '$ ? (@ > 1 && @ < 5 || @ == 7)'::jsonpath, '$ ? (!(@ > 1))'::jsonpath, '$ ? ((@ > 1) is unknown)'::jsonpath, '$ ? (exists (@.a))'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $?(@ > 1),$?(@ > 1 && @ < 5 || @ == 7),$?(!(@ > 1)),$?((@ > 1) is unknown),$?(exists (@."a")) +SELECT 1 +> SELECT '$.a starts with "x"'::jsonpath, '$.a starts with $v'::jsonpath, '$ like_regex "a.c"'::jsonpath, '$ like_regex "a" flag "ismq"'::jsonpath, '$ like_regex "a" flag "iiq"'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row ($."a" starts with "x"),($."a" starts with $"v"),($ like_regex "a.c"),($ like_regex "a" flag "ismq"),($ like_regex "a" flag "iq") +SELECT 1 +> SELECT '$.a[1]'::jsonpath, '$.a[1, 2 to 3, last]'::jsonpath, '$.a[last - 1]'::jsonpath, '$[$.a]'::jsonpath, '$.**{2}'::jsonpath, '$.**{2 to 5}'::jsonpath, '$.**{last}'::jsonpath, '$.**{1 to last}'::jsonpath, '$.**{last to 2}'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."a"[1],$."a"[1,2 to 3,last],$."a"[last - 1],$[$."a"],$.**{2},$.**{2 to 5},$.**{last},$.**{1 to last},$.**{last to 2} +SELECT 1 +> SELECT '$.type()'::jsonpath, '$.size().abs().floor().ceiling().double().keyvalue()'::jsonpath, '$.bigint().boolean().date().integer().number().string()'::jsonpath, '$.lower().upper().initcap()'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $.type(),$.size().abs().floor().ceiling().double().keyvalue(),$.bigint().boolean().date().integer().number().string(),$.lower().upper().initcap() +SELECT 1 +> SELECT '$.datetime()'::jsonpath, '$.datetime("HH24")'::jsonpath, '$.time()'::jsonpath, '$.time(3)'::jsonpath, '$.time_tz(2)'::jsonpath, '$.timestamp()'::jsonpath, '$.timestamp_tz(1)'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $.datetime(),$.datetime("HH24"),$.time(),$.time(3),$.time_tz(2),$.timestamp(),$.timestamp_tz(1) +SELECT 1 +> SELECT '$.decimal()'::jsonpath, '$.decimal(4)'::jsonpath, '$.decimal(4, 2)'::jsonpath, '$.decimal(+4, -2)'::jsonpath, '$.replace("a", "b")'::jsonpath, '$.split_part(",", 2)'::jsonpath, '$.split_part(",", -2)'::jsonpath, '$.ltrim()'::jsonpath, '$.rtrim("x")'::jsonpath, '$.btrim(" ")'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $.decimal(),$.decimal(4),$.decimal(4,2),$.decimal(4,-2),$.replace("a","b"),$.split_part(",",2),$.split_part(",",-2),$.ltrim(),$.rtrim("x"),$.btrim(" ") +SELECT 1 +> SELECT '$.type'::jsonpath, '$.Type()'::jsonpath, '$.TYPE'::jsonpath, 'STRICT $'::jsonpath, '$.null'::jsonpath, '$.NULL'::jsonpath, '$.true'::jsonpath, '$.last'::jsonpath, '$.to.is.with.flag'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."type",$.type(),$."TYPE",strict $,$."null",$."NULL",$."true",$."last",$."to"."is"."with"."flag" +SELECT 1 +> SELECT 'null'::jsonpath, 'true'::jsonpath, 'false'::jsonpath, '"abc"'::jsonpath, '$var'::jsonpath, '$"my var"'::jsonpath, '@'::text, '1.5'::jsonpath, '.5'::jsonpath, '5.'::jsonpath, '1e3'::jsonpath, '1.5e-3'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,text:25:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row null,true,false,"abc",$"var",$"my var",@,1.5,0.5,5,1000,0.0015 +SELECT 1 +> SELECT '0x1F'::jsonpath, '0o17'::jsonpath, '0b101'::jsonpath, '1_000'::jsonpath, '1_000.5_5'::jsonpath, '0'::jsonpath, '00'::text +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,text:25:-1:-1 +row 31,15,5,1000,1000.55,0,00 +SELECT 1 +> SELECT '(1).a'::jsonpath, '(1 + 2).a'::jsonpath, '($.a).b'::jsonpath, '($ > 1).a'::jsonpath, '"x".type()'::jsonpath, '$.a + $.b * $.c'::jsonpath, '$.a - ($.b - $.c)'::jsonpath, '($.a - $.b) - $.c'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row (1)."a",(1 + 2)."a",$."a"."b",($ > 1)."a","x".type(),($."a" + $."b" * $."c"),($."a" - ($."b" - $."c")),(($."a" - $."b") - $."c") +SELECT 1 +> SELECT '$."\n\t\"\\x"'::jsonpath, '$.ab'::jsonpath, '"é\x41\u{1F600}"'::jsonpath, '$."\b\f\r\v"'::jsonpath, '$.\a'::jsonpath, '"\/"'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."\n\t\"\\x",$."ab","éA😀",$."\b\f\r\u000b",$."a","/" +SELECT 1 +> SELECT '$ ? (@.a == 1 && (@.b == 2 || @.c == 3))'::jsonpath, '$ ? (@.a == 1 || @.b == 2 && @.c == 3)'::jsonpath, '$ ? ((@.a == 1 || @.b == 2) && @.c == 3)'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $?(@."a" == 1 && (@."b" == 2 || @."c" == 3)),$?(@."a" == 1 || @."b" == 2 && @."c" == 3),$?((@."a" == 1 || @."b" == 2) && @."c" == 3) +SELECT 1 +> SELECT '$ ? (@ == "x" && !(@ starts with "y"))'::jsonpath, '$ ? (@ like_regex "^a" flag "i").b'::jsonpath, '$[*] ? (@ > 1) ? (@ < 5)'::jsonpath, '$.a ? (@ == $x).b[0]'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $?(@ == "x" && !(@ starts with "y")),$?(@ like_regex "^a" flag "i")."b",$[*]?(@ > 1)?(@ < 5),$."a"?(@ == $"x")."b"[0] +SELECT 1 +> SELECT ''::jsonpath +ERROR 22P02 invalid input syntax for type jsonpath: "" at 8 +> SELECT ' '::jsonpath +ERROR 22P02 invalid input syntax for type jsonpath: " " at 8 +> SELECT '/* x */'::jsonpath +ERROR 22P02 invalid input syntax for type jsonpath: "/* x */" at 8 +> SELECT 'strict'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$ abc'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$ abc def'::jsonpath +ERROR 42601 syntax error at or near " " of jsonpath input at 8 +> SELECT '$.a b'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$a b'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.a.'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$ +'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.1'::jsonpath +ERROR 42601 syntax error at or near ".1" of jsonpath input at 8 +> SELECT '1a'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "1a" of jsonpath input at 8 +> SELECT '1.a'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "1.a" of jsonpath input at 8 +> SELECT '1e'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "1e" of jsonpath input at 8 +> SELECT '1e+'::jsonpath +ERROR 42601 invalid numeric literal at or near "1e+" of jsonpath input at 8 +> SELECT '1__0'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '0x'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "0x" of jsonpath input at 8 +> SELECT '0x1g'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '"abc'::jsonpath +ERROR 42601 unterminated quoted string at end of jsonpath input at 8 +> SELECT '$."abc'::jsonpath +ERROR 42601 unterminated quoted string at end of jsonpath input at 8 +> SELECT '/* abc'::jsonpath +ERROR 42601 unexpected end of comment at end of jsonpath input at 8 +> SELECT '"\u12"'::jsonpath +ERROR 42601 invalid Unicode escape sequence at or near "\u12" of jsonpath input at 8 +> SELECT '"\x1"'::jsonpath +ERROR 42601 invalid hexadecimal character sequence at or near "\x1" of jsonpath input at 8 +> SELECT '"\u0000"'::jsonpath +ERROR 22P05 unsupported Unicode escape sequence at 8 +> SELECT '"\ud83d"'::jsonpath +ERROR 22P02 invalid input syntax for type jsonpath at 8 +> SELECT '"\ude00"'::jsonpath +ERROR 22P02 invalid input syntax for type jsonpath at 8 +> SELECT '"😀"'::jsonpath +fields jsonpath:4072:-1:-1 +row "😀" +SELECT 1 +> SELECT '"abc\'::jsonpath +ERROR 42601 unexpected end after backslash at or near "\" of jsonpath input at 8 +> SELECT '@'::jsonpath +ERROR 42601 @ is not allowed in root expressions at 8 +> SELECT '$.a[@]'::jsonpath +ERROR 42601 @ is not allowed in root expressions at 8 +> SELECT 'last'::jsonpath +ERROR 42601 LAST is allowed only in array subscripts at 8 +> SELECT '$ ? (last > 1)'::jsonpath +ERROR 42601 LAST is allowed only in array subscripts at 8 +> SELECT '$[last ? (@ > last)]'::jsonpath +fields jsonpath:4072:-1:-1 +row $[last?(@ > last)] +SELECT 1 +> SELECT '@ + last'::jsonpath +ERROR 42601 @ is not allowed in root expressions at 8 +> SELECT 'last + @'::jsonpath +ERROR 42601 LAST is allowed only in array subscripts at 8 +> SELECT '$.decimal(1, 2, 3)'::jsonpath +ERROR 42601 invalid input syntax for type jsonpath at 8 +> SELECT '$ like_regex "a" flag "z"'::jsonpath +ERROR 42601 invalid input syntax for type jsonpath at 8 +> SELECT '$ like_regex "a" flag "x"'::jsonpath +ERROR 0A000 XQuery "x" flag (expanded regular expressions) is not implemented at 8 +> SELECT '$ like_regex "a" flag "qx"'::jsonpath +fields jsonpath:4072:-1:-1 +row ($ like_regex "a" flag "xq") +SELECT 1 +> SELECT '$ like_regex "(a"'::jsonpath +ERROR 2201B invalid regular expression: parentheses () not balanced at 8 +> SELECT '$.**{99999999999}'::jsonpath +ERROR 22003 value "99999999999" is out of range for type integer at 8 +> SELECT '$ > 1 > 2'::jsonpath +ERROR 42601 syntax error at or near ">" of jsonpath input at 8 +> SELECT '($ > 1) + 2'::jsonpath +ERROR 42601 syntax error at or near "+" of jsonpath input at 8 +> SELECT '!($.a)'::jsonpath +ERROR 42601 syntax error at or near ")" of jsonpath input at 8 +> SELECT '!($ > 1).a'::jsonpath +ERROR 42601 syntax error at or near "." of jsonpath input at 8 +> SELECT '!($ > 1) is unknown'::jsonpath +ERROR 42601 syntax error at or near " " of jsonpath input at 8 +> SELECT '(1) is unknown'::jsonpath +ERROR 42601 syntax error at or near " " of jsonpath input at 8 +> SELECT 'exists($ > 1)'::jsonpath +ERROR 42601 syntax error at or near ">" of jsonpath input at 8 +> SELECT 'exists(($ > 1))'::jsonpath +ERROR 42601 syntax error at or near ")" of jsonpath input at 8 +> SELECT '-($ > 1)'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$ ? (true)'::jsonpath +ERROR 42601 syntax error at or near ")" of jsonpath input at 8 +> SELECT '$ ? (@ == TRUE)'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.a && $.b'::jsonpath +ERROR 42601 syntax error at or near "&&" of jsonpath input at 8 +> SELECT '$.type('::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.time("x")'::jsonpath +ERROR 42601 syntax error at or near """ of jsonpath input at 8 +> SELECT '$[1, *]'::jsonpath +ERROR 42601 syntax error at or near "*" of jsonpath input at 8 +> SELECT '$ = 1'::jsonpath +ERROR 42601 syntax error at or near "=" of jsonpath input at 8 +> SELECT '$ ** 2'::jsonpath +ERROR 42601 syntax error at or near "**" of jsonpath input at 8 +> SELECT '$.** * 2'::jsonpath +fields jsonpath:4072:-1:-1 +row ($.** * 2) +SELECT 1 +> SELECT '$ & 1'::jsonpath +ERROR 42601 syntax error at or near "&" of jsonpath input at 8 +> SELECT '$#'::jsonpath +ERROR 42601 syntax error at or near "#" of jsonpath input at 8 +> SELECT '$.a::text'::jsonpath +ERROR 42601 syntax error at or near ":" of jsonpath input at 8 +> SELECT '$.a{1}'::jsonpath +ERROR 42601 syntax error at or near "{" of jsonpath input at 8 +> SELECT '$.a/*c*/.b'::jsonpath +ERROR 42601 syntax error at or near "." of jsonpath input at 8 +> SELECT '123abc'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '00'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "00" of jsonpath input at 8 +> SELECT '1e5x'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "1e5x" of jsonpath input at 8 +> SELECT '1e5xy'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '1.5e3abc'::jsonpath +ERROR 42601 trailing junk after numeric literal at or near "1.5e3a" of jsonpath input at 8 +> SELECT '$ ? (@ == 1e1000000)'::jsonpath +ERROR 22003 value overflows numeric format at 8 +> SELECT '$.**{0x10}'::jsonpath, '$.**{1_0 to 0b11}'::jsonpath, '$.**{0}'::jsonpath, '$.**{0 to last}'::jsonpath, '$.**{last to last}'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $.**{16},$.**{10 to 3},$.**{0},$.**,$.**{last} +SELECT 1 +> SELECT '"\u{110000}"'::jsonpath +ERROR 42601 invalid Unicode code point at 8 +> SELECT pg_input_is_valid('"\u{110000}"', 'jsonpath') +fields pg_input_is_valid:16:1:-1 +row f +SELECT 1 +> SELECT * FROM pg_input_error_info('"\u{110000}"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row could not convert Unicode to server encoding,NULL,NULL,42601 +SELECT 1 +> SELECT * FROM pg_input_error_info('"\ud83d"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,Unicode low surrogate must follow a high surrogate.,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('"\ud83dx"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,Unicode low surrogate must follow a high surrogate.,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('"\ude00"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,Unicode low surrogate must follow a high surrogate.,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('"\ud83d\ud83d"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,Unicode high surrogate must not follow a high surrogate.,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('"\u0000"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row unsupported Unicode escape sequence,\u0000 cannot be converted to text.,NULL,22P05 +SELECT 1 +> SELECT * FROM pg_input_error_info('$.decimal(1, 2, 3)', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,.decimal() can only have an optional precision[,scale].,NULL,42601 +SELECT 1 +> SELECT * FROM pg_input_error_info('$ like_regex "a" flag "iw"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,Unrecognized flag character "w" in LIKE_REGEX predicate.,NULL,42601 +SELECT 1 +> SELECT * FROM pg_input_error_info('$ like_regex "a" flag "é"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath,Unrecognized flag character "é" in LIKE_REGEX predicate.,NULL,42601 +SELECT 1 +> SELECT * FROM pg_input_error_info('$ like_regex "(a"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid regular expression: parentheses () not balanced,NULL,NULL,2201B +SELECT 1 +> SELECT * FROM pg_input_error_info('$ like_regex "a" flag "x"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row XQuery "x" flag (expanded regular expressions) is not implemented,NULL,NULL,0A000 +SELECT 1 +> SELECT * FROM pg_input_error_info('', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid input syntax for type jsonpath: "",NULL,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('$ ? (@ > 1', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row syntax error at end of jsonpath input,NULL,NULL,42601 +SELECT 1 +> SELECT * FROM pg_input_error_info('@', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row @ is not allowed in root expressions,NULL,NULL,42601 +SELECT 1 +> SELECT pg_input_is_valid('$.a', 'jsonpath'), pg_input_is_valid('$.a +', 'jsonpath'), pg_input_is_valid('last', 'jsonpath') +fields pg_input_is_valid:16:1:-1,pg_input_is_valid:16:1:-1,pg_input_is_valid:16:1:-1 +row t,f,f +SELECT 1 +> SELECT pg_input_is_valid('$.**{99999999999}', 'jsonpath') +fields pg_input_is_valid:16:1:-1 +ERROR 22003 value "99999999999" is out of range for type integer +> SELECT '"ሴ5"'::jsonpath, '"\u{1234567}"'::text +fields jsonpath:4072:-1:-1,text:25:-1:-1 +row "ሴ5","\u{1234567}" +SELECT 1 +> SELECT '"\u{1234567}"'::jsonpath +ERROR 42601 invalid Unicode escape sequence at or near "\u{123456" of jsonpath input at 8 +> SELECT '"\uZ"'::jsonpath +ERROR 42601 invalid Unicode escape sequence at or near "\u" of jsonpath input at 8 +> SELECT '"\xZ"'::jsonpath +ERROR 42601 invalid hexadecimal character sequence at or near "\x" of jsonpath input at 8 +> SELECT '"\x41\x7a"'::jsonpath, '"a\qb\é"'::jsonpath, '$.Abc'::jsonpath, '$.aB'::jsonpath, '$.\x41'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row "Az","aqbé",$."Abc",$."aB",$."A" +SELECT 1 +> SELECT '$.true'::jsonpath, '$var.a'::jsonpath, '$1'::jsonpath, '$"x\"y"'::jsonpath, '$"x"[0]'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."true",$"var"."a",$"1",$"x\"y",$"x"[0] +SELECT 1 +> SELECT '$."a"."b"'::jsonpath, '$.a."b c".d'::jsonpath, '$."".a'::jsonpath, '$.é'::jsonpath, '$.a1_b'::jsonpath, '$._x'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."a"."b",$."a"."b c"."d",$.""."a",$."é",$."a1_b",$."_x" +SELECT 1 +> SELECT 'strict $.a ? (@.b == 1).c'::jsonpath, 'LAX $'::jsonpath, 'Strict $'::jsonpath, 'lax 1'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row strict $."a"?(@."b" == 1)."c",$,strict $,1 +SELECT 1 +> SELECT '$.a[1 + 2, $.b to last]'::jsonpath, '$[last - 1 to last]'::jsonpath, '$[(1)]'::jsonpath, '$[1].a[2]'::jsonpath, '$[*][*]'::jsonpath, '$.a[*].**{1}.*'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."a"[1 + 2,$."b" to last],$[last - 1 to last],$[1],$[1]."a"[2],$[*][*],$."a"[*].**{1}.* +SELECT 1 +> SELECT '-1 * 2'::jsonpath, '-$.a * 2'::jsonpath, '-(1).a'::jsonpath, '+1'::jsonpath, '-0'::jsonpath, '- - $'::jsonpath, '-(-$)'::jsonpath, '1 - -1'::jsonpath, '1 - - $'::jsonpath, '(1 * 2) * 3'::jsonpath, '1 * (2 * 3)'::jsonpath, '1 / 2 % 3'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row (-1 * 2),(-$."a" * 2),(-(1)."a"),1,0,(-(-$)),(-(-$)),(1 - -1),(1 - -$),((1 * 2) * 3),(1 * (2 * 3)),((1 / 2) % 3) +SELECT 1 +> SELECT '$ ? (@ like_regex "a" && @ starts with "b")'::jsonpath, '$ ? ((@ + 1) like_regex "a")'::jsonpath, '$ ? (-@ like_regex "a")'::jsonpath, '$ ? (@ like_regex "a" flag "")'::jsonpath, '$ ? (@ like_regex "a\\d" flag "q")'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $?(@ like_regex "a" && @ starts with "b"),$?((@ + 1) like_regex "a"),$?((-@) like_regex "a"),$?(@ like_regex "a"),$?(@ like_regex "a\\d" flag "q") +SELECT 1 +> SELECT '$ ? (!exists(@.a))'::jsonpath, '$ ? (exists(@ ? (@ > 1)))'::jsonpath, '$ ? (!(@ == 1) && ((@ == 2) is unknown))'::jsonpath, 'exists($)'::jsonpath, '$ == 1'::jsonpath, '($ == 1) is unknown'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $?(!(exists (@."a"))),$?(exists (@?(@ > 1))),$?(!(@ == 1) && (@ == 2) is unknown),exists ($),($ == 1),($ == 1) is unknown +SELECT 1 +> SELECT '$.a ? ($.b > 1)'::jsonpath, '$ ? (@.a ? (@ > 1) > 2)'::jsonpath, '1 + $'::jsonpath, '(1 + $).a'::jsonpath, '($).a'::jsonpath, '(($.a))'::jsonpath, '((1 == 1))'::jsonpath, '(($ > 1)).a'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."a"?($."b" > 1),$?(@."a"?(@ > 1) > 2),(1 + $),(1 + $)."a",$."a",$."a",(1 == 1),($ > 1)."a" +SELECT 1 +> SELECT '$.datetime().time().type()'::jsonpath, '$.split_part("", +1)'::jsonpath, '$.decimal(0, 0)'::jsonpath, '$.replace("\n", "\"")'::jsonpath, '$.time(0x10)'::jsonpath, '$.decimal(1_0)'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $.datetime().time().type(),$.split_part("",1),$.decimal(0,0),$.replace("\n","\""),$.time(16),$.decimal(10) +SELECT 1 +> SELECT '$.exists'::jsonpath, '$.like_regex'::jsonpath, '$.starts'::jsonpath, '$.unknown'::jsonpath, '$.strict'::jsonpath, '$.lax'::jsonpath, '$.datetime'::jsonpath, '$.decimal'::jsonpath, '$.split_part'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."exists",$."like_regex",$."starts",$."unknown",$."strict",$."lax",$."datetime",$."decimal",$."split_part" +SELECT 1 +> SELECT '$.a'::jsonpath::text, '$.a'::text::jsonpath, ARRAY['$.a', 'strict $[*]']::jsonpath[], '$'::jsonpath IS NULL +fields text:25:-1:-1,jsonpath:4072:-1:-1,array:4073:-1:-1,?column?:16:1:-1 +row $."a",$."a",{"$.\"a\"","strict $[*]"},f +SELECT 1 +> SELECT '$ ? (@ == null || @ == true || @ == false || @ == "s" || @ == 1.5 || @ == $v)'::jsonpath +fields jsonpath:4072:-1:-1 +row $?(((((@ == null || @ == true) || @ == false) || @ == "s") || @ == 1.5) || @ == $"v") +SELECT 1 +> SELECT '$.time(-1)'::jsonpath +ERROR 42601 syntax error at or near "-" of jsonpath input at 8 +> SELECT '$.decimal(1.5)'::jsonpath +ERROR 42601 syntax error at or near "1.5" of jsonpath input at 8 +> SELECT '$.split_part("a", "b")'::jsonpath +ERROR 42601 syntax error at or near """ of jsonpath input at 8 +> SELECT '$.replace("a")'::jsonpath +ERROR 42601 syntax error at or near ")" of jsonpath input at 8 +> SELECT '$.**{-1}'::jsonpath +ERROR 42601 syntax error at or near "-" of jsonpath input at 8 +> SELECT '$.**{1'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$[1 2]'::jsonpath +ERROR 42601 syntax error at or near "2" of jsonpath input at 8 +> SELECT '$[]'::jsonpath +ERROR 42601 syntax error at or near "]" of jsonpath input at 8 +> SELECT '$ ? ()'::jsonpath +ERROR 42601 syntax error at or near ")" of jsonpath input at 8 +> SELECT '$ ? (@ starts "a")'::jsonpath +ERROR 42601 syntax error at or near """ of jsonpath input at 8 +> SELECT '$ ? (@ starts with 1)'::jsonpath +ERROR 42601 syntax error at or near "1" of jsonpath input at 8 +> SELECT '$ ? (@ like_regex 1)'::jsonpath +ERROR 42601 syntax error at or near "1" of jsonpath input at 8 +> SELECT '$ ? (@ like_regex "a" flag 1)'::jsonpath +ERROR 42601 syntax error at or near "1" of jsonpath input at 8 +> SELECT '$ ? (@ like_regex "a" junk)'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$ like_regex "(a" flag "z"'::jsonpath +ERROR 42601 invalid input syntax for type jsonpath at 8 +> SELECT '$ like_regex "(a" junk'::jsonpath +ERROR 2201B invalid regular expression: parentheses () not balanced at 8 +> SELECT '$.decimal(1, 2, 3) junk'::jsonpath +ERROR 42601 invalid input syntax for type jsonpath at 8 +> SELECT '$ ? (@ > 1) is unknown'::jsonpath +ERROR 42601 syntax error at or near " " of jsonpath input at 8 +> SELECT '$ ? ((@ > 1) is unknown is unknown)'::jsonpath +ERROR 42601 syntax error at or near " " of jsonpath input at 8 +> SELECT 'lax strict $'::jsonpath +ERROR 42601 syntax error at or near " " of jsonpath input at 8 +> SELECT '$ strict'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT 'true && false'::jsonpath +ERROR 42601 syntax error at or near "&&" of jsonpath input at 8 +> SELECT '!true'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.a.type ()'::jsonpath +fields jsonpath:4072:-1:-1 +row $."a".type() +SELECT 1 +> SELECT '$.a. b'::jsonpath, '$ .a'::jsonpath, '$ [1]'::jsonpath, '$ ? (@)'::text +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,text:25:-1:-1 +row $."a"."b",$."a",$[1],$ ? (@) +SELECT 1 +> SELECT '$.a.type ( )'::jsonpath +fields jsonpath:4072:-1:-1 +row $."a".type() +SELECT 1 +> SELECT '$ ? (@ == "a" "b")'::jsonpath +ERROR 42601 syntax error at or near """ of jsonpath input at 8 +> SELECT 'é'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.a == $.b == 1'::jsonpath +ERROR 42601 syntax error at or near "==" of jsonpath input at 8 +> SELECT '|| 1'::jsonpath +ERROR 42601 syntax error at or near "||" of jsonpath input at 8 +> SELECT '$ /* c */ .a'::jsonpath, '$.a /*c*/'::jsonpath, '$ . a'::jsonpath, ' $ '::jsonpath, '$/**/.a'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."a",$."a",$."a",$,$."a" +SELECT 1 +> SELECT '$.a/***/[1]'::jsonpath +ERROR 42601 syntax error at or near "[" of jsonpath input at 8 +> SELECT '$.a/*c*/'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '"😀A"'::jsonpath, '"\u{41}\u{1F600}"'::jsonpath, '"\uD83D\uDE00\u0041"'::jsonpath, '"\u{0041}\\"'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row "😀A","A😀","😀A","A\\" +SELECT 1 +> SELECT '$."A\"'::jsonpath +ERROR 42601 unterminated quoted string at end of jsonpath input at 8 +> SELECT '"\ud83d\u0041"'::jsonpath +ERROR 22P02 invalid input syntax for type jsonpath at 8 +> SELECT '$.a\u0041\x42\n'::jsonpath, '$."\u0041"'::jsonpath, '$.\u0041 ? (@ > 1)'::jsonpath +fields jsonpath:4072:-1:-1,jsonpath:4072:-1:-1,jsonpath:4072:-1:-1 +row $."aAB\n",$."A",$."A"?(@ > 1) +SELECT 1 +> SELECT '$\u0041'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$x\u0041'::jsonpath +ERROR 42601 syntax error at end of jsonpath input at 8 +> SELECT '$.a\'::jsonpath +ERROR 42601 unexpected end after backslash at or near "\" of jsonpath input at 8 +> SELECT '"\u{}"'::jsonpath +ERROR 42601 invalid Unicode escape sequence at or near "\u{" of jsonpath input at 8 +> SELECT '"\u{12345678}"'::jsonpath +ERROR 42601 invalid Unicode escape sequence at or near "\u{123456" of jsonpath input at 8 +> SELECT pg_input_is_valid('1e1000000', 'jsonpath') +fields pg_input_is_valid:16:1:-1 +ERROR 22003 value overflows numeric format +> SELECT '1e1000000'::jsonpath +ERROR 22003 value overflows numeric format at 8 +> SELECT pg_typeof('$'::jsonpath), '$.a'::jsonpath::text = '$."a"', ARRAY['$.a ? (@ > 1)']::jsonpath[]::text, NULL::jsonpath IS NULL +fields pg_typeof:2206:4:-1,?column?:16:1:-1,array:25:-1:-1,?column?:16:1:-1 +row jsonpath,t,{"$.\"a\"?(@ > 1)"},t +SELECT 1 +> SELECT '$.a'::jsonpath::text || '!', length('$.**{1 to 2}'::jsonpath::text) +fields ?column?:25:-1:-1,length:23:4:-1 +row $."a"!,12 +SELECT 1 +> SELECT x::jsonpath FROM (VALUES ('$.b'), ('strict $.a[last]'), ('$ ? (@.c like_regex "^x" flag "im")')) AS v(x) +fields x:4072:-1:-1 +row $."b" +row strict $."a"[last] +row $?(@."c" like_regex "^x" flag "im") +SELECT 3 +> SELECT * FROM pg_input_error_info('$.a/*', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row unexpected end of comment at end of jsonpath input,NULL,NULL,42601 +SELECT 1 +> SELECT * FROM pg_input_error_info('$ ? (@ like_regex "[")', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid regular expression: brackets [] not balanced,NULL,NULL,2201B +SELECT 1 +> SELECT * FROM pg_input_error_info('$."\u{110000}"', 'jsonpath') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row could not convert Unicode to server encoding,NULL,NULL,42601 +SELECT 1 +> COPY (SELECT '$.a ? (@ > 1)'::jsonpath AS a, 'strict $[*]'::jsonpath AS b) TO STDOUT +copy out 0 0,0 +data $.\"a\"?(@ > 1)\tstrict $[*]\n +copy done +COPY 1 +> COPY (SELECT '$.a ? (@ > 1)'::jsonpath AS a, 'strict $[*]'::jsonpath AS b) TO STDOUT WITH (FORMAT binary) +copy out 1 1,1 +data PGCOPY\n\xff\r\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0e\x01$.\"a\"?(@ > 1)\x00\x00\x00\x0c\x01strict $[*] +data \xff\xff +copy done +COPY 1 +P q1|SELECT $1::jsonpath +X q1|$.a.b +row $."a"."b" +SELECT 1 +X q1|strict $ ? (@ like_regex "x") +row strict $?(@ like_regex "x") +SELECT 1 +X q1|$ + +ERROR 42601 syntax error at end of jsonpath input (unnamed portal parameter $1 = '...') diff --git a/crates/rupg-types/src/jsonb.rs b/crates/rupg-types/src/jsonb.rs index 971afba0..76a49bea 100644 --- a/crates/rupg-types/src/jsonb.rs +++ b/crates/rupg-types/src/jsonb.rs @@ -151,7 +151,7 @@ fn unique(fields: &mut Vec<(String, usize)>) { } /// `escape_json`: a string in quotes, with the escapes that PostgreSQL writes. -fn escape(text: &str, out: &mut String) { +pub(crate) fn escape(text: &str, out: &mut String) { out.push('"'); for c in text.chars() { match c { diff --git a/crates/rupg-types/src/jsonpath.rs b/crates/rupg-types/src/jsonpath.rs new file mode 100644 index 00000000..de85f0be --- /dev/null +++ b/crates/rupg-types/src/jsonpath.rs @@ -0,0 +1,1731 @@ +//! The `jsonpath` type, from `jsonpath_scan.l`, `jsonpath_gram.y` and `jsonpath.c`. A value is the tree of the path, as `flattenJsonPathParseItem` writes it. The text form is the text of `printJsonPathItem`, and the binary form is the version byte 1 and the text form. +//! +//! The scanner gives the tokens that the scanner of flex gives: the longest match wins, and the first rule wins a tie. The text of a token is the text that flex has in `yytext`, so a syntax error names the same text as PostgreSQL. The parser follows the grammar, and it stops at the first token that the grammar cannot take, as bison does. + +use rupg_common::SqlState; + +use crate::binary::Recv; +use crate::error::TypeError; +use crate::jsonb::escape; +use crate::number::int4_in; +use crate::numeric::{Numeric, numeric_in, numeric_out}; +use crate::tsquery::too_deep; + +/// `JSP_REGEX_ICASE`: the flag `i` of `like_regex`. +pub const JSONPATH_REGEX_ICASE: u8 = 0x01; + +/// `JSP_REGEX_DOTALL`: the flag `s` of `like_regex`. +pub const JSONPATH_REGEX_DOTALL: u8 = 0x02; + +/// `JSP_REGEX_MLINE`: the flag `m` of `like_regex`. +pub const JSONPATH_REGEX_MLINE: u8 = 0x04; + +/// `JSP_REGEX_WSPACE`: the flag `x` of `like_regex`. +pub const JSONPATH_REGEX_WSPACE: u8 = 0x08; + +/// `JSP_REGEX_QUOTE`: the flag `q` of `like_regex`. +pub const JSONPATH_REGEX_QUOTE: u8 = 0x10; + +/// The bits of `cflags` in `regex.h` that [`jsonpath_regex_flags`] gives. +const REG_ADVANCED: u32 = 0o3; +const REG_QUOTE: u32 = 0o4; +const REG_ICASE: u32 = 0o10; +const REG_NLSTOP: u32 = 0o100; +const REG_NLANCH: u32 = 0o200; + +/// The deepest nesting of parentheses, operators and filters. PostgreSQL stops a deep path when it reaches `max_stack_depth`. The parser here works by recursion, and this limit keeps it on the stack. It is the limit of the analyzer. +const MAX_DEPTH: usize = 1000; + +/// The `last` bound of `.**{}`, as `PG_UINT32_MAX`. +pub const JSONPATH_LAST: u32 = u32::MAX; + +/// An operator or a method of a path, as `JsonPathItemType` has it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PathOp { + And, + Or, + Not, + IsUnknown, + Equal, + NotEqual, + Less, + Greater, + LessOrEqual, + GreaterOrEqual, + Add, + Sub, + Mul, + Div, + Mod, + Plus, + Minus, + Filter, + Exists, + StartsWith, + Type, + Size, + Abs, + Floor, + Ceiling, + Double, + Datetime, + KeyValue, + Bigint, + Boolean, + Date, + Decimal, + Integer, + Number, + StringFunc, + Time, + TimeTz, + Timestamp, + TimestampTz, + Replace, + Lower, + Upper, + Ltrim, + Rtrim, + Btrim, + Initcap, + SplitPart, +} + +impl PathOp { + /// `jspOperationName`: the name of the operator or the method in the text form and in the errors of the executor. The operators `!`, `is unknown`, `?` and `exists` have no name there, and the name here is their text. + #[must_use] + pub fn name(self) -> &'static str { + match self { + PathOp::And => "&&", + PathOp::Or => "||", + PathOp::Not => "!", + PathOp::IsUnknown => "is unknown", + PathOp::Equal => "==", + PathOp::NotEqual => "!=", + PathOp::Less => "<", + PathOp::Greater => ">", + PathOp::LessOrEqual => "<=", + PathOp::GreaterOrEqual => ">=", + PathOp::Add | PathOp::Plus => "+", + PathOp::Sub | PathOp::Minus => "-", + PathOp::Mul => "*", + PathOp::Div => "/", + PathOp::Mod => "%", + PathOp::Filter => "?", + PathOp::Exists => "exists", + PathOp::StartsWith => "starts with", + PathOp::Type => "type", + PathOp::Size => "size", + PathOp::Abs => "abs", + PathOp::Floor => "floor", + PathOp::Ceiling => "ceiling", + PathOp::Double => "double", + PathOp::Datetime => "datetime", + PathOp::KeyValue => "keyvalue", + PathOp::Bigint => "bigint", + PathOp::Boolean => "boolean", + PathOp::Date => "date", + PathOp::Decimal => "decimal", + PathOp::Integer => "integer", + PathOp::Number => "number", + PathOp::StringFunc => "string", + PathOp::Time => "time", + PathOp::TimeTz => "time_tz", + PathOp::Timestamp => "timestamp", + PathOp::TimestampTz => "timestamp_tz", + PathOp::Replace => "replace", + PathOp::Lower => "lower", + PathOp::Upper => "upper", + PathOp::Ltrim => "ltrim", + PathOp::Rtrim => "rtrim", + PathOp::Btrim => "btrim", + PathOp::Initcap => "initcap", + PathOp::SplitPart => "split_part", + } + } + + /// `operationPriority`: an operand with a priority that is not higher than the priority of its operator is in parentheses in the text form. + fn priority(self) -> u8 { + match self { + PathOp::Or => 0, + PathOp::And => 1, + PathOp::Equal + | PathOp::NotEqual + | PathOp::Less + | PathOp::Greater + | PathOp::LessOrEqual + | PathOp::GreaterOrEqual + | PathOp::StartsWith => 2, + PathOp::Add | PathOp::Sub => 3, + PathOp::Mul | PathOp::Div | PathOp::Mod => 4, + PathOp::Plus | PathOp::Minus => 5, + _ => 6, + } + } +} + +/// One item of a path. The items that follow an item, its `next` items in PostgreSQL, are the items after it in its [`Chain`]. +#[derive(Clone, Debug, PartialEq)] +pub enum PathItem { + /// `null`. + Null, + /// A string literal. + String(String), + /// A number literal. + Numeric(Numeric), + /// `true` or `false`. + Bool(bool), + /// `&&`, `||`, a comparison, an arithmetic operator or `starts with`, with the left and the right operand. + Binary(PathOp, Chain, Chain), + /// `!`, `is unknown`, unary `+` and `-`, the filter `?()` and `exists`, with the operand. + Unary(PathOp, Chain), + /// `like_regex`: the operand, the pattern and the bits of the flags. + LikeRegex { expr: Chain, pattern: String, flags: u8 }, + /// `[*]`. + AnyArray, + /// `.*`. + AnyKey, + /// `[...]`: each subscript, with the end of a range `to`. + IndexArray(Vec<(Chain, Option)>), + /// `.**`, with the first and the last level. [`JSONPATH_LAST`] is `last`. + Any { first: u32, last: u32 }, + /// `.key`. + Key(String), + /// `@`. + Current, + /// `$`. + Root, + /// `$name`. + Variable(String), + /// `last`. + Last, + /// A method such as `.type()`, with its arguments. An argument is a string or a number. + Method(PathOp, Vec), +} + +impl PathItem { + /// The priority of the item as an operand. + fn priority(&self) -> u8 { + match self { + PathItem::Binary(op, ..) | PathItem::Unary(op, _) => op.priority(), + _ => 6, + } + } +} + +/// An item and the items that follow it. A chain is never empty. +pub type Chain = Vec; + +/// A value of `jsonpath`. +#[derive(Clone, Debug, PartialEq)] +pub struct JsonPath { + /// False for `strict`. + pub lax: bool, + /// The path. + pub expr: Chain, +} + +/// `jspConvertRegexFlags`: the `cflags` of the regular expression of `like_regex` with the flags `flags`. +/// +/// # Errors +/// +/// `0A000` for the flag `x` with no flag `q`. +pub fn jsonpath_regex_flags(flags: u8) -> Result { + let mut cflags = REG_ADVANCED; + if flags & JSONPATH_REGEX_ICASE != 0 { + cflags |= REG_ICASE; + } + if flags & JSONPATH_REGEX_QUOTE != 0 { + cflags &= !REG_ADVANCED; + cflags |= REG_QUOTE; + } else { + if flags & JSONPATH_REGEX_DOTALL == 0 { + cflags |= REG_NLSTOP; + } + if flags & JSONPATH_REGEX_MLINE != 0 { + cflags |= REG_NLANCH; + } + if flags & JSONPATH_REGEX_WSPACE != 0 { + return Err(TypeError::new( + SqlState::FEATURE_NOT_SUPPORTED, + "XQuery \"x\" flag (expanded regular expressions) is not implemented".to_string(), + )); + } + } + Ok(cflags) +} + +/// The check of a regular expression: the pattern and the `cflags`, and the message of `pg_regerror` for a pattern that is not valid. +pub type RegexCheck<'a> = &'a dyn Fn(&str, u32) -> Result<(), String>; + +/// The error `22P02` of the type with a detail. +fn bad_input(detail: &str) -> TypeError { + let mut error = TypeError::new( + SqlState::INVALID_TEXT_REPRESENTATION, + "invalid input syntax for type jsonpath".to_string(), + ); + error.detail = Some(detail.to_string()); + error +} + +/// `jsonpath_yyerror`: the message and the text of the token, or the end of the input when the text is empty. +fn yyerror(message: &str, text: &[u8]) -> TypeError { + let message = if text.is_empty() { + format!("{message} at end of jsonpath input") + } else { + format!("{message} at or near \"{}\" of jsonpath input", String::from_utf8_lossy(text)) + }; + TypeError::new(SqlState::SYNTAX_ERROR, message) +} + +/// `special`: a character that is a token by itself. +fn is_special(b: u8) -> bool { + b"?%$.[]{}()|&!=<>@#,*:-+/".contains(&b) +} + +/// `blank`. +fn is_blank(b: u8) -> bool { + matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0c) +} + +/// `other`: a byte that is not special, not blank and not `\` or `"`. +fn is_other(b: u8) -> bool { + !is_special(b) && !is_blank(b) && b != b'\\' && b != b'"' +} + +/// A key word of the scanner. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Kw { + Is, + To, + Lax, + Flag, + Last, + Null, + True, + With, + False, + Exists, + Starts, + Strict, + Unknown, + LikeRegex, + /// A key word that names a method. + Method(PathOp), +} + +/// `keywords`: the key words, and true for a key word that matches only in lower case. +const KEYWORDS: [(&str, bool, Kw); 41] = [ + ("is", false, Kw::Is), + ("to", false, Kw::To), + ("abs", false, Kw::Method(PathOp::Abs)), + ("lax", false, Kw::Lax), + ("date", false, Kw::Method(PathOp::Date)), + ("flag", false, Kw::Flag), + ("last", false, Kw::Last), + ("null", true, Kw::Null), + ("size", false, Kw::Method(PathOp::Size)), + ("time", false, Kw::Method(PathOp::Time)), + ("true", true, Kw::True), + ("type", false, Kw::Method(PathOp::Type)), + ("with", false, Kw::With), + ("btrim", false, Kw::Method(PathOp::Btrim)), + ("false", true, Kw::False), + ("floor", false, Kw::Method(PathOp::Floor)), + ("lower", false, Kw::Method(PathOp::Lower)), + ("ltrim", false, Kw::Method(PathOp::Ltrim)), + ("rtrim", false, Kw::Method(PathOp::Rtrim)), + ("upper", false, Kw::Method(PathOp::Upper)), + ("bigint", false, Kw::Method(PathOp::Bigint)), + ("double", false, Kw::Method(PathOp::Double)), + ("exists", false, Kw::Exists), + ("number", false, Kw::Method(PathOp::Number)), + ("starts", false, Kw::Starts), + ("strict", false, Kw::Strict), + ("string", false, Kw::Method(PathOp::StringFunc)), + ("boolean", false, Kw::Method(PathOp::Boolean)), + ("ceiling", false, Kw::Method(PathOp::Ceiling)), + ("decimal", false, Kw::Method(PathOp::Decimal)), + ("initcap", false, Kw::Method(PathOp::Initcap)), + ("integer", false, Kw::Method(PathOp::Integer)), + ("replace", false, Kw::Method(PathOp::Replace)), + ("time_tz", false, Kw::Method(PathOp::TimeTz)), + ("unknown", false, Kw::Unknown), + ("datetime", false, Kw::Method(PathOp::Datetime)), + ("keyvalue", false, Kw::Method(PathOp::KeyValue)), + ("timestamp", false, Kw::Method(PathOp::Timestamp)), + ("like_regex", false, Kw::LikeRegex), + ("split_part", false, Kw::Method(PathOp::SplitPart)), + ("timestamp_tz", false, Kw::Method(PathOp::TimestampTz)), +]; + +/// `checkKeyword`: the key word that a name is, if any. +fn keyword(name: &[u8]) -> Option { + KEYWORDS.iter().find_map(|&(word, lower, kw)| { + let found = if lower { + name == word.as_bytes() + } else { + name.eq_ignore_ascii_case(word.as_bytes()) + }; + found.then_some(kw) + }) +} + +/// The kind of a token. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Tok { + End, + And, + Or, + Not, + /// `**`. + AnyLevels, + Less, + LessEqual, + Equal, + NotEqual, + GreaterEqual, + Greater, + /// A special character. + Char(u8), + Variable, + Str, + Numeric, + Int, + Ident, + Kw(Kw), +} + +/// A token: the kind, the bounds of the text of flex in the input, and the string of a name, a string, a variable or a number. +struct Token { + tok: Tok, + text: (usize, usize), + value: Vec, +} + +/// The rules of the start state of the scanner that can match at a position, in the order of `jsonpath_scan.l`. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Rule { + Op(Tok, usize), + Variable, + QuotedVariable, + Special, + Blank, + Comment, + Numeric, + Int, + RealFail, + Junk, + Quote, + Backslash, + Name, +} + +/// The rules of an escape in a name or a string, in the order of `jsonpath_scan.l`. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Escape { + Char(u8), + Unicode, + Hex, + UnicodeFail, + HexFail, + UnicodeBackslash, + Any, + Lone, +} + +/// The scanner, at a position of the input. +struct Lexer<'a> { + s: &'a [u8], + at: usize, + /// True for the soft input of `pg_input_is_valid`, which changes the error for a code point that is too large. + soft: bool, +} + +impl Lexer<'_> { + fn byte(&self, i: usize) -> u8 { + self.s.get(i).copied().unwrap_or(0) + } + + fn has(&self, i: usize, f: fn(u8) -> bool) -> bool { + i < self.s.len() && f(self.s[i]) + } + + /// The ends of the matches of `(_?{digit})*` after a digit that ends at `q`. + fn digits_from(&self, mut q: usize, digit: fn(u8) -> bool, ends: &mut Vec) { + ends.push(q); + loop { + if self.has(q, digit) { + q += 1; + } else if self.byte(q) == b'_' && self.has(q + 1, digit) { + q += 2; + } else { + return; + } + ends.push(q); + } + } + + /// The ends of the matches of `decinteger` at `p`. + fn decinteger(&self, p: usize) -> Vec { + let mut ends = Vec::new(); + match self.byte(p) { + b'0' => ends.push(p + 1), + b'1'..=b'9' => self.digits_from(p + 1, |b| b.is_ascii_digit(), &mut ends), + _ => {} + } + ends + } + + /// The ends of the matches of `decdigits` at `p`. + fn decdigits(&self, p: usize, ends: &mut Vec) { + if self.has(p, |b| b.is_ascii_digit()) { + self.digits_from(p + 1, |b| b.is_ascii_digit(), ends); + } + } + + /// The ends of the matches of `decimal` at `p`. + fn decimal(&self, p: usize) -> Vec { + let mut ends = Vec::new(); + for end in self.decinteger(p) { + if self.byte(end) == b'.' { + ends.push(end + 1); + self.decdigits(end + 1, &mut ends); + } + } + if self.byte(p) == b'.' { + self.decdigits(p + 1, &mut ends); + } + ends + } + + /// The ends of the matches of `real` at `p`, and of `realfail`. + fn real(&self, p: usize) -> (Vec, Vec) { + let (mut ends, mut fails) = (Vec::new(), Vec::new()); + let mut bases = self.decinteger(p); + bases.extend(self.decimal(p)); + for base in bases { + if matches!(self.byte(base), b'e' | b'E') { + self.decdigits(base + 1, &mut ends); + if matches!(self.byte(base + 1), b'+' | b'-') { + fails.push(base + 2); + self.decdigits(base + 2, &mut ends); + } + } + } + (ends, fails) + } + + /// The end of the match of `0x`, `0o` or `0b` and its digits at `p`. + fn radix(&self, p: usize, letter: u8, digit: fn(u8) -> bool) -> Option { + if self.byte(p) == b'0' + && self.byte(p + 1).to_ascii_lowercase() == letter + && self.has(p + 2, digit) + { + let mut ends = Vec::new(); + self.digits_from(p + 3, digit, &mut ends); + ends.last().copied() + } else { + None + } + } + + /// The end of a match of a rule `{x}_junk`: the longest match of `x` that one `other` byte follows. + fn junk(&self, ends: &[usize]) -> Option { + ends.iter().filter(|&&end| self.has(end, is_other)).map(|&end| end + 1).max() + } + + /// The end of a run of bytes from `p` that `f` takes. + fn run(&self, mut p: usize, f: fn(u8) -> bool) -> usize { + while self.has(p, f) { + p += 1; + } + p + } + + /// The longest rule of the start state at `p`, and the end of its match. The first rule wins a tie. + fn longest(&self, p: usize) -> (Rule, usize) { + let mut best: Option<(Rule, usize)> = None; + let mut offer = |rule: Rule, end: Option| { + if let Some(end) = end + && best.is_none_or(|(_, e)| end > e) + { + best = Some((rule, end)); + } + }; + let ops: [(&[u8], Tok); 11] = [ + (b"&&", Tok::And), + (b"||", Tok::Or), + (b"!", Tok::Not), + (b"**", Tok::AnyLevels), + (b"<", Tok::Less), + (b"<=", Tok::LessEqual), + (b"==", Tok::Equal), + (b"<>", Tok::NotEqual), + (b"!=", Tok::NotEqual), + (b">=", Tok::GreaterEqual), + (b">", Tok::Greater), + ]; + for (text, tok) in ops { + offer( + Rule::Op(tok, text.len()), + self.s[p..].starts_with(text).then_some(p + text.len()), + ); + } + let dollar = self.byte(p) == b'$'; + offer( + Rule::Variable, + (dollar && self.has(p + 1, is_other)).then(|| self.run(p + 1, is_other)), + ); + offer(Rule::QuotedVariable, (dollar && self.byte(p + 1) == b'"').then_some(p + 2)); + offer(Rule::Special, self.has(p, is_special).then_some(p + 1)); + offer(Rule::Blank, self.has(p, is_blank).then(|| self.run(p, is_blank))); + offer(Rule::Comment, self.s[p..].starts_with(b"/*").then_some(p + 2)); + let (real, realfail) = self.real(p); + let decimal = self.decimal(p); + let decinteger = self.decinteger(p); + offer(Rule::Numeric, real.iter().max().copied()); + offer(Rule::Numeric, decimal.iter().max().copied()); + offer(Rule::Int, decinteger.iter().max().copied()); + offer(Rule::Int, self.radix(p, b'x', |b| b.is_ascii_hexdigit())); + offer(Rule::Int, self.radix(p, b'o', |b| matches!(b, b'0'..=b'7'))); + offer(Rule::Int, self.radix(p, b'b', |b| matches!(b, b'0' | b'1'))); + offer(Rule::RealFail, realfail.iter().max().copied()); + offer(Rule::Junk, self.junk(&decinteger)); + offer(Rule::Junk, self.junk(&decimal)); + offer(Rule::Junk, self.junk(&real)); + offer(Rule::Quote, (self.byte(p) == b'"' && p < self.s.len()).then_some(p + 1)); + // The rule `\\` gives back its match, so it ties with nothing. + offer(Rule::Backslash, (self.byte(p) == b'\\' && p < self.s.len()).then_some(p + 1)); + offer(Rule::Name, self.has(p, is_other).then(|| self.run(p, is_other))); + best.unwrap_or((Rule::Special, p)) + } + + /// The next token. A name that a comment follows gives no token, as in PostgreSQL. + fn next(&mut self) -> Result { + loop { + let p = self.at; + if p >= self.s.len() { + return Ok(Token { tok: Tok::End, text: (p, p), value: Vec::new() }); + } + let (rule, end) = self.longest(p); + let token = |tok| Token { tok, text: (p, end), value: self.s[p..end].to_vec() }; + self.at = end; + match rule { + Rule::Op(tok, _) => return Ok(token(tok)), + Rule::Variable => { + return Ok(Token { + tok: Tok::Variable, + text: (p, end), + value: self.s[p + 1..end].to_vec(), + }); + } + Rule::QuotedVariable => return self.quoted(Tok::Variable), + Rule::Special => return Ok(token(Tok::Char(self.s[p]))), + Rule::Blank => {} + Rule::Comment => self.comment()?, + Rule::Numeric => return Ok(token(Tok::Numeric)), + Rule::Int => return Ok(token(Tok::Int)), + Rule::RealFail => return Err(yyerror("invalid numeric literal", &self.s[p..end])), + Rule::Junk => { + return Err(yyerror("trailing junk after numeric literal", &self.s[p..end])); + } + Rule::Quote => return self.quoted(Tok::Str), + Rule::Backslash => { + self.at = p; + if let Some(token) = self.name(Vec::new())? { + return Ok(token); + } + } + Rule::Name => { + if let Some(token) = self.name(self.s[p..end].to_vec())? { + return Ok(token); + } + } + } + } + } + + /// The state `xc`: the rest of a comment. + fn comment(&mut self) -> Result<(), TypeError> { + match self.s[self.at..].windows(2).position(|w| w == b"*/") { + Some(at) => { + self.at += at + 2; + Ok(()) + } + None => { + self.at = self.s.len(); + Err(yyerror("unexpected end of comment", b"")) + } + } + } + + /// The state `xnq`: the rest of a name. The result is `None` when a comment follows the name, as the scanner of PostgreSQL then drops the name. + fn name(&mut self, mut value: Vec) -> Result, TypeError> { + loop { + let p = self.at; + let end_token = |value: Vec, text| { + let tok = keyword(&value).map_or(Tok::Ident, Tok::Kw); + Some(Token { tok, text, value }) + }; + if p >= self.s.len() { + return Ok(end_token(value, (p, p))); + } + let b = self.s[p]; + if is_other(b) { + self.at = self.run(p, is_other); + value.extend_from_slice(&self.s[p..self.at]); + } else if is_blank(b) { + self.at = self.run(p, is_blank); + return Ok(end_token(value, (p, self.at))); + } else if self.s[p..].starts_with(b"/*") { + self.at = p + 2; + self.comment()?; + return Ok(None); + } else if b == b'\\' { + self.escape(&mut value)?; + } else { + return Ok(end_token(value, (p, p))); + } + } + } + + /// The states `xq` and `xvq`: the rest of a quoted string or variable. + fn quoted(&mut self, tok: Tok) -> Result { + let mut value = Vec::new(); + loop { + let p = self.at; + match self.s.get(p) { + None => return Err(yyerror("unterminated quoted string", b"")), + Some(b'"') => { + self.at = p + 1; + return Ok(Token { tok, text: (p, p + 1), value }); + } + Some(b'\\') => self.escape(&mut value)?, + Some(_) => { + self.at = self.run(p, |b| b != b'\\' && b != b'"'); + value.extend_from_slice(&self.s[p..self.at]); + } + } + } + } + + /// The end of a match of `unicode` at `p`. + fn unicode(&self, p: usize) -> Option { + if self.byte(p) != b'\\' || self.byte(p + 1) != b'u' { + return None; + } + if self.byte(p + 2) == b'{' { + let end = self.run(p + 3, |b| b.is_ascii_hexdigit()).min(p + 9); + (end > p + 3 && self.byte(end) == b'}').then_some(end + 1) + } else { + (p + 2..p + 6).all(|i| self.has(i, |b| b.is_ascii_hexdigit())).then_some(p + 6) + } + } + + /// The end of the longest match of `unicodefail` at `p`. + fn unicode_fail(&self, p: usize) -> Option { + if self.byte(p) != b'\\' || self.byte(p + 1) != b'u' { + return None; + } + Some(if self.byte(p + 2) == b'{' { + self.run(p + 3, |b| b.is_ascii_hexdigit()).min(p + 9) + } else { + self.run(p + 2, |b| b.is_ascii_hexdigit()).min(p + 5) + }) + } + + /// An escape at the backslash at `self.at`, with the rules of `jsonpath_scan.l`. + fn escape(&mut self, value: &mut Vec) -> Result<(), TypeError> { + let p = self.at; + let mut bounds = vec![p]; + while let Some(end) = self.unicode(bounds[bounds.len() - 1]) { + bounds.push(end); + } + let chain = bounds[bounds.len() - 1]; + let next = self.s.get(p + 1).copied(); + let mut best: Option<(Escape, usize)> = None; + let mut offer = |rule: Escape, end: Option| { + if let Some(end) = end + && best.is_none_or(|(_, e)| end > e) + { + best = Some((rule, end)); + } + }; + let simple = match next { + Some(b'b') => Some(0x08), + Some(b'f') => Some(0x0c), + Some(b'n') => Some(b'\n'), + Some(b'r') => Some(b'\r'), + Some(b't') => Some(b'\t'), + Some(b'v') => Some(0x0b), + _ => None, + }; + if let Some(c) = simple { + offer(Escape::Char(c), Some(p + 2)); + } + offer(Escape::Unicode, (chain > p).then_some(chain)); + let hex = next == Some(b'x'); + offer( + Escape::Hex, + (hex && (p + 2..p + 4).all(|i| self.has(i, |b| b.is_ascii_hexdigit()))) + .then_some(p + 4), + ); + offer(Escape::UnicodeFail, bounds.iter().filter_map(|&at| self.unicode_fail(at)).max()); + offer(Escape::HexFail, hex.then(|| self.run(p + 2, |b| b.is_ascii_hexdigit()).min(p + 3))); + offer( + Escape::UnicodeBackslash, + (chain > p && self.byte(chain) == b'\\').then_some(chain + 1), + ); + offer(Escape::Any, next.filter(|&b| b != b'\n').map(|_| p + 2)); + offer(Escape::Lone, Some(p + 1)); + let (rule, end) = best.unwrap_or((Escape::Lone, p + 1)); + let text = &self.s[p..end]; + match rule { + Escape::Char(c) => value.push(c), + Escape::Unicode | Escape::UnicodeBackslash => { + self.unicode_chain(&bounds, value)?; + self.at = chain; + return Ok(()); + } + Escape::Hex => { + let code = u32::from_str_radix(std::str::from_utf8(&text[2..]).unwrap_or("0"), 16) + .unwrap_or(0); + self.add_char(code, value)?; + } + Escape::UnicodeFail => return Err(yyerror("invalid Unicode escape sequence", text)), + Escape::HexFail => return Err(yyerror("invalid hexadecimal character sequence", text)), + Escape::Any => value.push(text[1]), + Escape::Lone => return Err(yyerror("unexpected end after backslash", text)), + } + self.at = end; + Ok(()) + } + + /// `parseUnicode`: the escapes `\u` between the bounds, with the pairs of surrogates. + fn unicode_chain(&self, bounds: &[usize], value: &mut Vec) -> Result<(), TypeError> { + let mut high: Option = None; + let low_must_follow = || bad_input("Unicode low surrogate must follow a high surrogate."); + for pair in bounds.windows(2) { + let digits = &self.s[pair[0] + 2..pair[1]]; + let digits = digits.strip_prefix(b"{").map_or(digits, |d| &d[..d.len() - 1]); + let mut code = + u32::from_str_radix(std::str::from_utf8(digits).unwrap_or("0"), 16).unwrap_or(0); + if (0xd800..=0xdbff).contains(&code) { + if high.is_some() { + return Err(bad_input( + "Unicode high surrogate must not follow a high surrogate.", + )); + } + high = Some(code); + continue; + } + if (0xdc00..=0xdfff).contains(&code) { + let Some(first) = high.take() else { return Err(low_must_follow()) }; + code = ((first & 0x3ff) << 10) + 0x10000 + (code & 0x3ff); + } else if high.is_some() { + return Err(low_must_follow()); + } + self.add_char(code, value)?; + } + if high.is_some() { + return Err(low_must_follow()); + } + Ok(()) + } + + /// `addUnicodeChar`: one code point in UTF-8. + fn add_char(&self, code: u32, value: &mut Vec) -> Result<(), TypeError> { + if code == 0 { + let mut error = TypeError::new( + SqlState::UNTRANSLATABLE_CHARACTER, + "unsupported Unicode escape sequence".to_string(), + ); + error.detail = Some("\\u0000 cannot be converted to text.".to_string()); + return Err(error); + } + let Some(c) = char::from_u32(code) else { + let message = if self.soft { + "could not convert Unicode to server encoding" + } else { + "invalid Unicode code point" + }; + return Err(TypeError::new(SqlState::SYNTAX_ERROR, message.to_string())); + }; + value.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes()); + Ok(()) + } +} + +/// The parser, with the token that it reads ahead. +struct Parser<'a> { + lexer: Lexer<'a>, + ahead: Option, + depth: usize, + regex: RegexCheck<'a>, +} + +/// The text of a token as a string. +fn text_of(value: Vec) -> String { + String::from_utf8(value).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()) +} + +impl Parser<'_> { + fn peek(&mut self) -> Result { + if self.ahead.is_none() { + self.ahead = Some(self.lexer.next()?); + } + Ok(self.ahead.as_ref().map_or(Tok::End, |t| t.tok)) + } + + fn next(&mut self) -> Result { + self.peek()?; + Ok(self.ahead.take().unwrap_or(Token { tok: Tok::End, text: (0, 0), value: Vec::new() })) + } + + /// The syntax error at the token that the parser reads ahead. + fn error(&mut self) -> TypeError { + match &self.ahead { + Some(token) => yyerror("syntax error", &self.lexer.s[token.text.0..token.text.1]), + None => yyerror("syntax error", b""), + } + } + + fn expect(&mut self, tok: Tok) -> Result { + if self.peek()? == tok { self.next() } else { Err(self.error()) } + } + + /// The guard of the depth of the recursion. + fn deeper( + &mut self, + f: impl FnOnce(&mut Self) -> Result, + ) -> Result { + if self.depth >= MAX_DEPTH { + return Err(too_deep()); + } + self.depth += 1; + let result = f(self); + self.depth -= 1; + result + } + + /// `expr_or_predicate`, or `predicate` when `pred` is true. The flag of the result is true for a predicate. + fn or(&mut self, pred: bool) -> Result<(Chain, bool), TypeError> { + let (mut left, mut is_pred) = self.and(pred)?; + while self.peek()? == Tok::Or { + if !is_pred { + return Err(self.error()); + } + self.next()?; + let right = self.and(true)?.0; + left = vec![PathItem::Binary(PathOp::Or, left, right)]; + is_pred = true; + } + Ok((left, is_pred)) + } + + fn and(&mut self, pred: bool) -> Result<(Chain, bool), TypeError> { + let (mut left, mut is_pred) = self.not(pred)?; + while self.peek()? == Tok::And { + if !is_pred { + return Err(self.error()); + } + self.next()?; + let right = self.not(true)?.0; + left = vec![PathItem::Binary(PathOp::And, left, right)]; + is_pred = true; + } + Ok((left, is_pred)) + } + + fn not(&mut self, pred: bool) -> Result<(Chain, bool), TypeError> { + if self.peek()? != Tok::Not { + return self.comparison(pred); + } + self.next()?; + self.deeper(|p| { + let arg = match p.peek()? { + Tok::Char(b'(') => { + p.next()?; + let arg = p.or(true)?.0; + p.expect(Tok::Char(b')'))?; + arg + } + Tok::Kw(Kw::Exists) => p.exists()?, + _ => return Err(p.error()), + }; + Ok((vec![PathItem::Unary(PathOp::Not, arg)], true)) + }) + } + + /// `EXISTS '(' expr ')'`. + fn exists(&mut self) -> Result { + self.next()?; + self.expect(Tok::Char(b'('))?; + let arg = self.expr()?; + self.expect(Tok::Char(b')'))?; + Ok(vec![PathItem::Unary(PathOp::Exists, arg)]) + } + + /// An `expr`, where a predicate cannot be. + fn expr(&mut self) -> Result { + Ok(self.additive(true)?.0) + } + + fn comparison(&mut self, pred: bool) -> Result<(Chain, bool), TypeError> { + if self.peek()? == Tok::Kw(Kw::Exists) { + return Ok((self.exists()?, true)); + } + let (left, is_pred) = self.additive(false)?; + if is_pred { + return Ok((left, true)); + } + let op = match self.peek()? { + Tok::Equal => PathOp::Equal, + Tok::NotEqual => PathOp::NotEqual, + Tok::Less => PathOp::Less, + Tok::Greater => PathOp::Greater, + Tok::LessEqual => PathOp::LessOrEqual, + Tok::GreaterEqual => PathOp::GreaterOrEqual, + Tok::Kw(Kw::Starts) => { + self.next()?; + self.expect(Tok::Kw(Kw::With))?; + let item = match self.peek()? { + Tok::Str => PathItem::String(text_of(self.next()?.value)), + Tok::Variable => PathItem::Variable(text_of(self.next()?.value)), + _ => return Err(self.error()), + }; + return Ok((vec![PathItem::Binary(PathOp::StartsWith, left, vec![item])], true)); + } + Tok::Kw(Kw::LikeRegex) => { + self.next()?; + let pattern = text_of(self.expect(Tok::Str)?.value); + let flags = if self.peek()? == Tok::Kw(Kw::Flag) { + self.next()?; + Some(text_of(self.expect(Tok::Str)?.value)) + } else { + None + }; + return Ok((vec![self.like_regex(left, pattern, flags.as_deref())?], true)); + } + _ if pred => return Err(self.error()), + _ => return Ok((left, false)), + }; + self.next()?; + let right = self.expr()?; + Ok((vec![PathItem::Binary(op, left, right)], true)) + } + + /// `makeItemLikeRegex`: the flags and the check of the pattern. + fn like_regex( + &self, + expr: Chain, + pattern: String, + flags: Option<&str>, + ) -> Result { + let mut bits = 0; + for c in flags.unwrap_or_default().chars() { + bits |= match c { + 'i' => JSONPATH_REGEX_ICASE, + 's' => JSONPATH_REGEX_DOTALL, + 'm' => JSONPATH_REGEX_MLINE, + 'x' => JSONPATH_REGEX_WSPACE, + 'q' => JSONPATH_REGEX_QUOTE, + c => { + let mut error = bad_input(""); + error.sqlstate = SqlState::SYNTAX_ERROR; + error.detail = Some(format!( + "Unrecognized flag character \"{c}\" in LIKE_REGEX predicate." + )); + return Err(error); + } + }; + } + let cflags = jsonpath_regex_flags(bits)?; + (self.regex)(&pattern, cflags).map_err(|message| { + TypeError::new( + SqlState::INVALID_REGULAR_EXPRESSION, + format!("invalid regular expression: {message}"), + ) + })?; + Ok(PathItem::LikeRegex { expr, pattern, flags: bits }) + } + + fn additive(&mut self, expr: bool) -> Result<(Chain, bool), TypeError> { + let (mut left, is_pred) = self.multiplicative(expr)?; + if is_pred { + return Ok((left, true)); + } + loop { + let op = match self.peek()? { + Tok::Char(b'+') => PathOp::Add, + Tok::Char(b'-') => PathOp::Sub, + _ => return Ok((left, false)), + }; + self.next()?; + let right = self.multiplicative(true)?.0; + left = vec![PathItem::Binary(op, left, right)]; + } + } + + fn multiplicative(&mut self, expr: bool) -> Result<(Chain, bool), TypeError> { + let (mut left, is_pred) = self.unary(expr)?; + if is_pred { + return Ok((left, true)); + } + loop { + let op = match self.peek()? { + Tok::Char(b'*') => PathOp::Mul, + Tok::Char(b'/') => PathOp::Div, + Tok::Char(b'%') => PathOp::Mod, + _ => return Ok((left, false)), + }; + self.next()?; + let right = self.unary(true)?.0; + left = vec![PathItem::Binary(op, left, right)]; + } + } + + /// Unary `+` and `-`, which `makeItemUnary` folds into a number. + fn unary(&mut self, expr: bool) -> Result<(Chain, bool), TypeError> { + self.deeper(|p| { + let op = match p.peek()? { + Tok::Char(b'+') => PathOp::Plus, + Tok::Char(b'-') => PathOp::Minus, + _ => return p.accessor_expr(expr), + }; + p.next()?; + let arg = p.unary(true)?.0; + Ok((unary(op, arg), false)) + }) + } + + /// `accessor_expr`, or a predicate in parentheses with no accessor. When `expr` is true, a predicate in parentheses must have an accessor. + fn accessor_expr(&mut self, expr: bool) -> Result<(Chain, bool), TypeError> { + let item = match self.peek()? { + Tok::Char(b'(') => { + self.next()?; + let (mut inner, is_pred) = self.or(false)?; + self.expect(Tok::Char(b')'))?; + if is_pred && !matches!(self.peek()?, Tok::Char(b'.' | b'[' | b'?')) { + if expr { + return Err(self.error()); + } + if self.peek()? == Tok::Kw(Kw::Is) { + self.next()?; + self.expect(Tok::Kw(Kw::Unknown))?; + return Ok((vec![PathItem::Unary(PathOp::IsUnknown, inner)], true)); + } + return Ok((inner, true)); + } + self.accessors(&mut inner)?; + return Ok((inner, false)); + } + Tok::Str => PathItem::String(text_of(self.next()?.value)), + Tok::Kw(Kw::Null) => { + self.next()?; + PathItem::Null + } + Tok::Kw(Kw::True | Kw::False) => PathItem::Bool(self.next()?.tok == Tok::Kw(Kw::True)), + Tok::Numeric | Tok::Int => PathItem::Numeric(number(self.next()?)?), + Tok::Variable => PathItem::Variable(text_of(self.next()?.value)), + Tok::Char(b'$') => { + self.next()?; + PathItem::Root + } + Tok::Char(b'@') => { + self.next()?; + PathItem::Current + } + Tok::Kw(Kw::Last) => { + self.next()?; + PathItem::Last + } + _ => return Err(self.error()), + }; + let mut chain = vec![item]; + self.accessors(&mut chain)?; + Ok((chain, false)) + } + + /// The `accessor_op` items after an item. + fn accessors(&mut self, chain: &mut Chain) -> Result<(), TypeError> { + loop { + match self.peek()? { + Tok::Char(b'.') => { + self.next()?; + let item = self.after_dot()?; + chain.push(item); + } + Tok::Char(b'[') => { + self.next()?; + if self.peek()? == Tok::Char(b'*') { + self.next()?; + self.expect(Tok::Char(b']'))?; + chain.push(PathItem::AnyArray); + continue; + } + let mut elems = Vec::new(); + loop { + let from = self.expr()?; + let to = if self.peek()? == Tok::Kw(Kw::To) { + self.next()?; + Some(self.expr()?) + } else { + None + }; + elems.push((from, to)); + if self.peek()? != Tok::Char(b',') { + break; + } + self.next()?; + } + self.expect(Tok::Char(b']'))?; + chain.push(PathItem::IndexArray(elems)); + } + Tok::Char(b'?') => { + self.next()?; + self.expect(Tok::Char(b'('))?; + let arg = self.deeper(|p| p.or(true))?.0; + self.expect(Tok::Char(b')'))?; + chain.push(PathItem::Unary(PathOp::Filter, arg)); + } + _ => return Ok(()), + } + } + } + + /// The accessor after `.`: a key, `*`, `**` or a method. + fn after_dot(&mut self) -> Result { + match self.peek()? { + Tok::Char(b'*') => { + self.next()?; + Ok(PathItem::AnyKey) + } + Tok::AnyLevels => { + self.next()?; + if self.peek()? != Tok::Char(b'{') { + return Ok(PathItem::Any { first: 0, last: JSONPATH_LAST }); + } + self.next()?; + let first = self.level()?; + let last = if self.peek()? == Tok::Kw(Kw::To) { + self.next()?; + self.level()? + } else { + first + }; + self.expect(Tok::Char(b'}'))?; + Ok(PathItem::Any { first, last }) + } + Tok::Ident | Tok::Str => Ok(PathItem::Key(text_of(self.next()?.value))), + Tok::Kw(kw) => { + let token = self.next()?; + match kw { + Kw::Method(op) if self.peek()? == Tok::Char(b'(') => { + self.next()?; + self.method(op) + } + _ => Ok(PathItem::Key(text_of(token.value))), + } + } + _ => Err(self.error()), + } + } + + /// `any_level`: a number or `last`. The number is an `int4`. + fn level(&mut self) -> Result { + match self.peek()? { + Tok::Int => { + let value = text_of(self.next()?.value); + Ok(u32::try_from(int4_in(&value)?).unwrap_or(0)) + } + Tok::Kw(Kw::Last) => { + self.next()?; + Ok(JSONPATH_LAST) + } + _ => Err(self.error()), + } + } + + /// The arguments of a method after `(`, and the `)`. + fn method(&mut self, op: PathOp) -> Result { + let mut args = Vec::new(); + match op { + PathOp::Decimal => { + if self.peek()? != Tok::Char(b')') { + args.push(self.int_elem()?); + while self.peek()? == Tok::Char(b',') { + self.next()?; + args.push(self.int_elem()?); + } + } + self.expect(Tok::Char(b')'))?; + if args.len() > 2 { + let mut error = + bad_input(".decimal() can only have an optional precision[,scale]."); + error.sqlstate = SqlState::SYNTAX_ERROR; + return Err(error); + } + return Ok(PathItem::Method(op, args)); + } + PathOp::Datetime | PathOp::Ltrim | PathOp::Rtrim | PathOp::Btrim => { + if self.peek()? == Tok::Str { + args.push(PathItem::String(text_of(self.next()?.value))); + } + } + PathOp::Time | PathOp::TimeTz | PathOp::Timestamp | PathOp::TimestampTz => { + if self.peek()? == Tok::Int { + args.push(PathItem::Numeric(number(self.next()?)?)); + } + } + PathOp::Replace | PathOp::SplitPart => { + args.push(PathItem::String(text_of(self.expect(Tok::Str)?.value))); + self.expect(Tok::Char(b','))?; + args.push(if op == PathOp::Replace { + PathItem::String(text_of(self.expect(Tok::Str)?.value)) + } else { + self.int_elem()? + }); + } + _ => {} + } + self.expect(Tok::Char(b')'))?; + Ok(PathItem::Method(op, args)) + } + + /// `int_elem`: an integer with an optional sign. + fn int_elem(&mut self) -> Result { + let negate = match self.peek()? { + Tok::Char(b'+') => false, + Tok::Char(b'-') => true, + Tok::Int => return Ok(PathItem::Numeric(number(self.next()?)?)), + _ => return Err(self.error()), + }; + self.next()?; + let value = number(self.expect(Tok::Int)?)?; + Ok(PathItem::Numeric(if negate { value.negate() } else { value })) + } +} + +/// `makeItemNumeric`: the number of a token. An error of `numeric_in` is a hard error. +fn number(token: Token) -> Result { + numeric_in(&text_of(token.value), -1) +} + +/// `makeItemUnary` for `+` and `-`: a number with no accessor takes the sign. +fn unary(op: PathOp, arg: Chain) -> Chain { + if let [PathItem::Numeric(value)] = arg.as_slice() { + if op == PathOp::Minus { + return vec![PathItem::Numeric(value.negate())]; + } + return arg; + } + vec![PathItem::Unary(op, arg)] +} + +/// The checks of `flattenJsonPathParseItem`: `@` must be in a filter and `last` must be in a subscript. The order of the checks is the order of PostgreSQL: an item, then its operands, then the next item. +fn check(chain: &[PathItem], nesting: u32, subscript: bool) -> Result<(), TypeError> { + for item in chain { + match item { + PathItem::Current if nesting == 0 => { + return Err(TypeError::new( + SqlState::SYNTAX_ERROR, + "@ is not allowed in root expressions".to_string(), + )); + } + PathItem::Last if !subscript => { + return Err(TypeError::new( + SqlState::SYNTAX_ERROR, + "LAST is allowed only in array subscripts".to_string(), + )); + } + PathItem::Binary(_, left, right) => { + check(left, nesting, subscript)?; + check(right, nesting, subscript)?; + } + PathItem::Unary(op, arg) => { + check(arg, nesting + u32::from(*op == PathOp::Filter), subscript)? + } + PathItem::LikeRegex { expr, .. } => check(expr, nesting, subscript)?, + PathItem::IndexArray(elems) => { + for (from, to) in elems { + check(from, nesting, true)?; + if let Some(to) = to { + check(to, nesting, true)?; + } + } + } + _ => {} + } + } + Ok(()) +} + +/// `jsonpath_in`: a path from its text. `soft` is true for the soft input of `pg_input_is_valid`, and `regex` checks the pattern of `like_regex`. +/// +/// # Errors +/// +/// `42601` and `22P02` for text that is not a path, `22P05` for `\u0000`, `0A000` for the flag `x`, `2201B` for a pattern that is not valid, `22003` for a number that is too large and `54001` for a path that is too deep. The last two are hard errors in PostgreSQL. +pub fn jsonpath_parse( + text: &str, + soft: bool, + regex: RegexCheck<'_>, +) -> Result { + let mut parser = + Parser { lexer: Lexer { s: text.as_bytes(), at: 0, soft }, ahead: None, depth: 0, regex }; + let lax = match parser.peek()? { + Tok::End => return Err(TypeError::syntax("jsonpath", text)), + Tok::Kw(Kw::Strict) => { + parser.next()?; + false + } + Tok::Kw(Kw::Lax) => { + parser.next()?; + true + } + _ => true, + }; + let expr = parser.or(false)?.0; + if parser.peek()? != Tok::End { + return Err(parser.error()); + } + check(&expr, 0, false)?; + Ok(JsonPath { lax, expr }) +} + +/// `printJsonPathItem` for a chain: the first item, then the items that follow it. +fn print_chain(chain: &[PathItem], brackets: bool, out: &mut String) { + for (i, item) in chain.iter().enumerate() { + print_item(item, i > 0, i > 0 || brackets, i + 1 < chain.len(), out); + } +} + +/// An operand, in parentheses when its priority is not higher than `priority`. +fn print_operand(chain: &[PathItem], priority: u8, out: &mut String) { + print_chain(chain, chain[0].priority() <= priority, out); +} + +/// `printJsonPathItem` for one item. +fn print_item(item: &PathItem, in_key: bool, brackets: bool, has_next: bool, out: &mut String) { + let open = |out: &mut String| { + if brackets { + out.push('('); + } + }; + let close = |out: &mut String| { + if brackets { + out.push(')'); + } + }; + let dot = |out: &mut String| { + if in_key { + out.push('.'); + } + }; + match item { + PathItem::Null => out.push_str("null"), + PathItem::String(s) => escape(s, out), + PathItem::Numeric(value) => { + let mut bytes = Vec::new(); + numeric_out(value, &mut bytes); + let text = String::from_utf8_lossy(&bytes); + if has_next { + out.push('('); + out.push_str(&text); + out.push(')'); + } else { + out.push_str(&text); + } + } + PathItem::Bool(b) => out.push_str(if *b { "true" } else { "false" }), + PathItem::Binary(op, left, right) => { + open(out); + print_operand(left, op.priority(), out); + out.push(' '); + out.push_str(op.name()); + out.push(' '); + print_operand(right, op.priority(), out); + close(out); + } + PathItem::Unary(op @ (PathOp::Plus | PathOp::Minus), arg) => { + open(out); + out.push_str(op.name()); + print_operand(arg, op.priority(), out); + close(out); + } + PathItem::Unary(op, arg) => { + let (before, after) = match op { + PathOp::Not => ("!(", ")"), + PathOp::IsUnknown => ("(", ") is unknown"), + PathOp::Filter => ("?(", ")"), + _ => ("exists (", ")"), + }; + out.push_str(before); + print_chain(arg, false, out); + out.push_str(after); + } + PathItem::LikeRegex { expr, pattern, flags } => { + open(out); + print_operand(expr, 6, out); + out.push_str(" like_regex "); + escape(pattern, out); + if *flags != 0 { + out.push_str(" flag \""); + for (bit, c) in [ + (JSONPATH_REGEX_ICASE, 'i'), + (JSONPATH_REGEX_DOTALL, 's'), + (JSONPATH_REGEX_MLINE, 'm'), + (JSONPATH_REGEX_WSPACE, 'x'), + (JSONPATH_REGEX_QUOTE, 'q'), + ] { + if flags & bit != 0 { + out.push(c); + } + } + out.push('"'); + } + close(out); + } + PathItem::AnyArray => out.push_str("[*]"), + PathItem::AnyKey => { + dot(out); + out.push('*'); + } + PathItem::IndexArray(elems) => { + out.push('['); + for (i, (from, to)) in elems.iter().enumerate() { + if i > 0 { + out.push(','); + } + print_chain(from, false, out); + if let Some(to) = to { + out.push_str(" to "); + print_chain(to, false, out); + } + } + out.push(']'); + } + PathItem::Any { first, last } => { + dot(out); + let bound = + |n: u32| if n == JSONPATH_LAST { "last".to_string() } else { n.to_string() }; + if *first == 0 && *last == JSONPATH_LAST { + out.push_str("**"); + } else if first == last { + out.push_str(&format!("**{{{}}}", bound(*first))); + } else { + out.push_str(&format!("**{{{} to {}}}", bound(*first), bound(*last))); + } + } + PathItem::Key(key) => { + dot(out); + escape(key, out); + } + PathItem::Current => out.push('@'), + PathItem::Root => out.push('$'), + PathItem::Variable(name) => { + out.push('$'); + escape(name, out); + } + PathItem::Last => out.push_str("last"), + PathItem::Method(op, args) => { + out.push('.'); + out.push_str(op.name()); + out.push('('); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + out.push(','); + } + print_item(arg, false, false, false, out); + } + out.push(')'); + } + } +} + +/// `jsonpath_out`: the text form of a path. +pub fn jsonpath_deparse(path: &JsonPath, out: &mut Vec) { + let mut text = String::new(); + if !path.lax { + text.push_str("strict "); + } + print_chain(&path.expr, true, &mut text); + out.extend_from_slice(text.as_bytes()); +} + +/// `jsonpath_send`: the version 1 and the text form. +pub fn jsonpath_send(path: &JsonPath, out: &mut Vec) { + out.push(1); + jsonpath_deparse(path, out); +} + +/// `jsonpath_recv`: a path from the version 1 and the text form. Each error is a hard error. +/// +/// # Errors +/// +/// `XX000` for another version, and the errors of [`jsonpath_parse`]. +pub fn jsonpath_recv(recv: &mut Recv<'_>, regex: RegexCheck<'_>) -> Result { + let version = recv.byte()?; + if version != 1 { + return Err(TypeError::new( + SqlState::INTERNAL_ERROR, + format!("unsupported jsonpath version number: {version}"), + )); + } + jsonpath_parse(recv.text()?, false, regex) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn any_regex(_: &str, _: u32) -> Result<(), String> { + Ok(()) + } + + fn paren_regex(pattern: &str, _: u32) -> Result<(), String> { + if pattern.contains('(') { Err("parentheses () not balanced".to_string()) } else { Ok(()) } + } + + fn out(text: &str) -> String { + let path = + jsonpath_parse(text, false, &any_regex).unwrap_or_else(|e| panic!("{text}: {e}")); + let mut bytes = Vec::new(); + jsonpath_deparse(&path, &mut bytes); + String::from_utf8(bytes).expect("utf8") + } + + fn err(text: &str) -> String { + match jsonpath_parse(text, false, &paren_regex) { + Ok(_) => panic!("{text} is valid"), + Err(e) => format!("{} {}", e.sqlstate.as_str(), e.message), + } + } + + #[test] + fn text_form() { + let cases = [ + ("1 + 2", "(1 + 2)"), + ("1 + 2 + 3", "((1 + 2) + 3)"), + ("1 * 2 + 3", "(1 * 2 + 3)"), + ("- -1", "1"), + ("-$.a", "(-$.\"a\")"), + ("-(1).a", "(-(1).\"a\")"), + ("-$.a * 2", "(-$.\"a\" * 2)"), + ("1 / 2 % 3", "((1 / 2) % 3)"), + ("$ ? (@ > 1)", "$?(@ > 1)"), + ("$ ? ((@ > 1) is unknown)", "$?((@ > 1) is unknown)"), + ("$ ? (exists (@.a))", "$?(exists (@.\"a\"))"), + ("$ ? (!exists(@.a))", "$?(!(exists (@.\"a\")))"), + ("$.a[1,2 to 3,last]", "$.\"a\"[1,2 to 3,last]"), + ("$.**{last to 2}", "$.**{last to 2}"), + ("$.**{0 to last}", "$.**"), + ("$.**{last to last}", "$.**{last}"), + ("$.decimal(4,-2)", "$.decimal(4,-2)"), + ("$.Type()", "$.type()"), + ("$.TYPE", "$.\"TYPE\""), + ("STRICT $", "strict $"), + ("lax 1", "1"), + (".5", "0.5"), + ("1e3", "1000"), + ("0x1F", "31"), + ("1_000.5_5", "1000.55"), + ("$var.a", "$\"var\".\"a\""), + ("(1 + $).a", "(1 + $).\"a\""), + ("(($ > 1)).a", "($ > 1).\"a\""), + ("\"\\v\\x41\\u{1F600}\"", "\"\\u000bA\u{1F600}\""), + ("\"\\ud83d\\ude00\"", "\"\u{1F600}\""), + ("$.\\x41", "$.\"A\""), + ("$ /* c */ .a", "$.\"a\""), + ("$ ? (@ like_regex \"a\" flag \"qx\")", "$?(@ like_regex \"a\" flag \"xq\")"), + ("$ ? (-@ like_regex \"a\")", "$?((-@) like_regex \"a\")"), + ("$[last ? (@ > last)]", "$[last?(@ > last)]"), + ("$.split_part(\"\", +1)", "$.split_part(\"\",1)"), + ("$.a.type ( )", "$.\"a\".type()"), + ]; + for (text, want) in cases { + assert_eq!(out(text), want, "{text}"); + } + } + + #[test] + fn errors() { + let cases = [ + ("", "22P02 invalid input syntax for type jsonpath: \"\""), + ("/* c */", "22P02 invalid input syntax for type jsonpath: \"/* c */\""), + ("$ abc def", "42601 syntax error at or near \" \" of jsonpath input"), + ("$.a/*c*/", "42601 syntax error at end of jsonpath input"), + ("$.a/*c*/.b", "42601 syntax error at or near \".\" of jsonpath input"), + ("$.1", "42601 syntax error at or near \".1\" of jsonpath input"), + ("1a", "42601 trailing junk after numeric literal at or near \"1a\" of jsonpath input"), + ("00", "42601 trailing junk after numeric literal at or near \"00\" of jsonpath input"), + ("1e5xy", "42601 syntax error at end of jsonpath input"), + ( + "1.5e3abc", + "42601 trailing junk after numeric literal at or near \"1.5e3a\" of jsonpath input", + ), + ("1e+", "42601 invalid numeric literal at or near \"1e+\" of jsonpath input"), + ("\"a", "42601 unterminated quoted string at end of jsonpath input"), + ("$ /* a", "42601 unexpected end of comment at end of jsonpath input"), + ( + "\"\\u12\"", + "42601 invalid Unicode escape sequence at or near \"\\u12\" of jsonpath input", + ), + ( + "\"\\u{1234567}\"", + "42601 invalid Unicode escape sequence at or near \"\\u{123456\" of jsonpath input", + ), + ( + "\"\\xZ\"", + "42601 invalid hexadecimal character sequence at or near \"\\x\" of jsonpath input", + ), + ("\"\\u0000\"", "22P05 unsupported Unicode escape sequence"), + ("\"\\ud83d\"", "22P02 invalid input syntax for type jsonpath"), + ("\"\\u{110000}\"", "42601 invalid Unicode code point"), + ("@", "42601 @ is not allowed in root expressions"), + ("$.a[@]", "42601 @ is not allowed in root expressions"), + ("last + @", "42601 LAST is allowed only in array subscripts"), + ("$ > 1 > 2", "42601 syntax error at or near \">\" of jsonpath input"), + ("($ > 1) + 2", "42601 syntax error at or near \"+\" of jsonpath input"), + ("!($.a)", "42601 syntax error at or near \")\" of jsonpath input"), + ("!($ > 1) is unknown", "42601 syntax error at or near \" \" of jsonpath input"), + ("exists($ > 1)", "42601 syntax error at or near \">\" of jsonpath input"), + ("exists(($ > 1))", "42601 syntax error at or near \")\" of jsonpath input"), + ("-($ > 1)", "42601 syntax error at end of jsonpath input"), + ("$ ? (true)", "42601 syntax error at or near \")\" of jsonpath input"), + ("$ ? (@ == TRUE)", "42601 syntax error at end of jsonpath input"), + ("$.time(\"x\")", "42601 syntax error at or near \"\"\" of jsonpath input"), + ("$[1, *]", "42601 syntax error at or near \"*\" of jsonpath input"), + ("$.decimal(1, 2, 3) junk", "42601 invalid input syntax for type jsonpath"), + ("$ like_regex \"(a\" flag \"z\"", "42601 invalid input syntax for type jsonpath"), + ( + "$ like_regex \"(a\" junk", + "2201B invalid regular expression: parentheses () not balanced", + ), + ( + "$ like_regex \"a\" flag \"x\"", + "0A000 XQuery \"x\" flag (expanded regular expressions) is not implemented", + ), + ("$.**{99999999999}", "22003 value \"99999999999\" is out of range for type integer"), + ("lax strict $", "42601 syntax error at or near \" \" of jsonpath input"), + ("strict", "42601 syntax error at end of jsonpath input"), + ]; + for (text, want) in cases { + assert_eq!(err(text), want, "{text}"); + } + let soft = jsonpath_parse("\"\\u{110000}\"", true, &any_regex).expect_err("soft"); + assert_eq!(soft.message, "could not convert Unicode to server encoding"); + } + + #[test] + fn binary_form() { + let path = jsonpath_parse("strict $.a ? (@ > 1)", false, &any_regex).expect("path"); + let mut bytes = Vec::new(); + jsonpath_send(&path, &mut bytes); + assert_eq!(bytes, b"\x01strict $.\"a\"?(@ > 1)"); + assert_eq!(jsonpath_recv(&mut Recv::new(&bytes), &any_regex).expect("recv"), path); + let error = jsonpath_recv(&mut Recv::new(b"\x02$"), &any_regex).expect_err("version"); + assert_eq!(error.message, "unsupported jsonpath version number: 2"); + } +} diff --git a/crates/rupg-types/src/lib.rs b/crates/rupg-types/src/lib.rs index edb02d98..2353d4ba 100644 --- a/crates/rupg-types/src/lib.rs +++ b/crates/rupg-types/src/lib.rs @@ -30,7 +30,7 @@ //! //! [`Recv`], the binary input of a `Bind` parameter, with the errors of PostgreSQL when the value is too short or too long. The binary output of the other types is the value in big-endian bytes. //! -//! [`Lexeme`], one lexeme of a `tsvector` with its positions, with [`tsvector_parse`], [`tsvector_deparse`], [`tsvector_recv`] and [`tsvector_send`]. [`tsvector_cmp`] is the order of the B-tree operator class, and [`unique_positions`] puts positions in order as the input function does. [`QueryItem`], one operand or operator of a `tsquery`, with [`tsquery_parse`], [`tsquery_deparse`], [`tsquery_recv`] and [`tsquery_send`]. [`tsquery_cmp`] is the order of the B-tree operator class. +//! [`Lexeme`], one lexeme of a `tsvector` with its positions, with [`tsvector_parse`], [`tsvector_deparse`], [`tsvector_recv`] and [`tsvector_send`]. [`tsvector_cmp`] is the order of the B-tree operator class, and [`unique_positions`] puts positions in order as the input function does. [`QueryItem`], one operand or operator of a `tsquery`, with [`tsquery_parse`], [`tsquery_deparse`], [`tsquery_recv`] and [`tsquery_send`]. [`tsquery_cmp`] is the order of the B-tree operator class. [`JsonPath`], a value of `jsonpath`, with [`jsonpath_parse`], [`jsonpath_deparse`], [`jsonpath_recv`] and [`jsonpath_send`]. //! //! [`unicode`], the four Unicode normalization forms, and [`tz`], the time zones of the IANA data. //! @@ -42,7 +42,7 @@ //! //! [`pg_ndistinct_in`] and [`pg_dependencies_in`] read the JSON text of the types of the extended statistics with the details of the errors of PostgreSQL, and give the bytes of the value. [`pg_ndistinct_out`] and [`pg_dependencies_out`] write the text from the bytes. //! -//! PostgreSQL parses `json` by recursion and stops a deep value with `stack depth limit exceeded` when it reaches `max_stack_depth`. [`json_in`] uses no recursion and takes a value at any depth. PostgreSQL stops a deep `tsquery` in the same way. [`tsquery_parse`] gives the same error for a query with 1000 levels, as the analyzer does for an expression. +//! PostgreSQL parses `json` by recursion and stops a deep value with `stack depth limit exceeded` when it reaches `max_stack_depth`. [`json_in`] uses no recursion and takes a value at any depth. PostgreSQL stops a deep `tsquery` in the same way. [`tsquery_parse`] gives the same error for a query with 1000 levels, as the analyzer does for an expression. [`jsonpath_parse`] has the same limit for a path. //! //! The text and binary forms are lifted from `crates/rudb-pgtypes` of tamnd/rudb at f5f7065a (spec/04 section 4.9). The `DataRow` encoder and the `Bind` decoder of that crate need the executor, so they come with rupg-server. @@ -59,6 +59,7 @@ mod generated; mod geo; mod json; mod jsonb; +mod jsonpath; mod lsn; mod mac; mod money; @@ -107,6 +108,11 @@ pub use generated::oids as oid; pub use geo::{GeoKind, geo_in, geo_out, geo_recv}; pub use json::json_in; pub use jsonb::{jsonb_in, jsonb_recv, jsonb_send}; +pub use jsonpath::{ + Chain, JSONPATH_LAST, JSONPATH_REGEX_DOTALL, JSONPATH_REGEX_ICASE, JSONPATH_REGEX_MLINE, + JSONPATH_REGEX_QUOTE, JSONPATH_REGEX_WSPACE, JsonPath, PathItem, PathOp, RegexCheck, + jsonpath_deparse, jsonpath_parse, jsonpath_recv, jsonpath_regex_flags, jsonpath_send, +}; pub use lsn::{pg_lsn_in, pg_lsn_out}; pub use mac::{ macaddr_in, macaddr_out, macaddr_recv, macaddr_to_macaddr8, macaddr8_in, macaddr8_out, diff --git a/crates/rupg-types/src/tsquery.rs b/crates/rupg-types/src/tsquery.rs index 04000f88..48ed0e28 100644 --- a/crates/rupg-types/src/tsquery.rs +++ b/crates/rupg-types/src/tsquery.rs @@ -55,7 +55,7 @@ fn is_operator(c: char) -> bool { } /// The error of `check_stack_depth`. -fn too_deep() -> TypeError { +pub(crate) fn too_deep() -> TypeError { let mut error = TypeError::new(SqlState::STATEMENT_TOO_COMPLEX, "stack depth limit exceeded".to_string()); error.hint = Some("Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.".to_string()); diff --git a/crates/rupg-types/src/value.rs b/crates/rupg-types/src/value.rs index 2ddeb0e2..a0a7a74c 100644 --- a/crates/rupg-types/src/value.rs +++ b/crates/rupg-types/src/value.rs @@ -4,6 +4,7 @@ use crate::array::Array; use crate::datetime::Interval; +use crate::jsonpath::JsonPath; use crate::network::Inet; use crate::numeric::Numeric; use crate::range::Range; @@ -55,6 +56,8 @@ pub enum Value { TsVector(Vec), /// A value of `tsquery`: the operands and operators in prefix order, or no items for a query with no lexemes. TsQuery(Vec), + /// A value of `jsonpath`. + JsonPath(Box), } /// The value of a composite type or of `record`, as a `HeapTupleHeader` datum. As that datum keeps the type of the row, a record keeps the type of each field, so that the output of an anonymous `record` can show the fields.