From e938026ef4726b8fc5cb59f49d1c03b6b64a7b09 Mon Sep 17 00:00:00 2001 From: tamnd Date: Tue, 13 Oct 2026 15:24:37 +0700 Subject: [PATCH] Text and binary forms of the range types --- crates/rupg-analyze/src/node.rs | 26 +- crates/rupg-analyze/src/poly.rs | 328 ++++++++++++++---- crates/rupg-analyze/src/transform.rs | 6 +- crates/rupg-analyze/src/types.rs | 15 + crates/rupg-func/src/compare.rs | 21 +- crates/rupg-func/src/io.rs | 90 +++++ crates/rupg-func/src/lib.rs | 2 + crates/rupg-func/src/math.rs | 2 +- crates/rupg-func/src/range.rs | 319 +++++++++++++++++ crates/rupg-pgcatalog/src/builtin.rs | 21 ++ crates/rupg-session/src/connection.rs | 6 + crates/rupg-session/src/connection/range.test | 234 +++++++++++++ crates/rupg-types/src/lib.rs | 5 + crates/rupg-types/src/range.rs | 297 ++++++++++++++++ crates/rupg-types/src/value.rs | 3 + 15 files changed, 1307 insertions(+), 68 deletions(-) create mode 100644 crates/rupg-func/src/range.rs create mode 100644 crates/rupg-session/src/connection/range.test create mode 100644 crates/rupg-types/src/range.rs diff --git a/crates/rupg-analyze/src/node.rs b/crates/rupg-analyze/src/node.rs index 7dacbc39..7d76c178 100644 --- a/crates/rupg-analyze/src/node.rs +++ b/crates/rupg-analyze/src/node.rs @@ -3,7 +3,7 @@ //! PostgreSQL stores these as `pg_node_tree` text that `nodeToString` writes. rupg stores its own text, which document 05 section 5.2 permits, because clients do not read it. The form looks like the form of PostgreSQL: `{NAME :field value ...}`. A constant keeps its value in a form that does not change with the settings of the session, so the expression reads back to the same tree. use rupg_common::{Error, Result}; -use rupg_types::{Array, ArrayDim, Inet, Interval, NetFamily, Record, Recv, Value}; +use rupg_types::{Array, ArrayDim, Inet, Interval, NetFamily, Range, Record, Recv, Value}; use crate::expr::{ BoolOp, BoolTest, Case, CastForm, Expr, ExprKind, Func, FuncForm, SqlValue, Subscript, Var, @@ -426,6 +426,13 @@ fn write_value(value: &Value, out: &mut String) { } out.push_str("))"); } + Value::Range(range) => { + out.push_str(&format!("(range {} ", range.flags)); + write_value(&range.lower, out); + out.push(' '); + write_value(&range.upper, out); + out.push(')'); + } } } @@ -817,6 +824,14 @@ fn value_of(item: &Item) -> Result { let values = list(values)?.iter().map(value_of).collect::>()?; Value::Record(Box::new(Record { types, values })) } + "range" => { + let [flags, lower, upper] = parts else { return Err(bad("a range value")) }; + Value::Range(Box::new(Range { + flags: number(atom(flags)?)?, + lower: value_of(lower)?, + upper: value_of(upper)?, + })) + } kind => return Err(bad(&format!("the value kind {kind}"))), }; Ok(value) @@ -866,6 +881,15 @@ mod tests { (Value::Interval(Interval { time: -1, day: 2, month: -3 }), oid::INTERVAL), (Value::Inet(rupg_types::inet_in("10.1.2.3/8", false).expect("inet")), oid::INET), (Value::Inet(rupg_types::inet_in("2001:db8::/32", true).expect("cidr")), oid::CIDR), + ( + Value::Range(Box::new(Range { + flags: rupg_types::RANGE_LB_INC, + lower: Value::Int4(1), + upper: Value::Int4(10), + })), + oid::INT4RANGE, + ), + (Value::Range(Box::new(Range::empty())), oid::DATERANGE), ]; for (value, ty) in values { round_trip(&constant(value, ty)); diff --git a/crates/rupg-analyze/src/poly.rs b/crates/rupg-analyze/src/poly.rs index 0c244cf5..45a0ce08 100644 --- a/crates/rupg-analyze/src/poly.rs +++ b/crates/rupg-analyze/src/poly.rs @@ -1,6 +1,4 @@ //! The polymorphic types of `parse_coerce.c`: `check_generic_type_consistency`, which tells if the actual types fit the polymorphic arguments of a function, and `enforce_generic_type_consistency`, which gives the actual argument types and the result type of a call. -//! -//! The range and multirange families need `pg_range`, which comes with the range types. Until then an argument of a range family does not fit. use rupg_common::{Error, Result, SqlState}; use rupg_types::oid; @@ -13,73 +11,107 @@ use crate::types; struct Families { element: u32, array: u32, + range: u32, + multirange: u32, nonarray: bool, is_enum: bool, + /// True when an argument or the result is `anymultirange`. + wants_multirange: bool, + /// The number of arguments of the first family: `anyelement`, `anyarray`, `anyrange` and the others. any_element_args: usize, compatible: Vec, compatible_array: bool, compatible_nonarray: bool, + compatible_range: u32, + compatible_range_element: u32, + compatible_multirange: u32, + /// True when an argument or the result is `anycompatiblerange`. + wants_compatible_range: bool, + /// True when an argument or the result is `anycompatiblemultirange`. + wants_compatible_multirange: bool, has_compatible: bool, - unknowns: bool, } /// The error of two arguments of one family with different types. -fn not_alike(family: &str) -> Error { +fn not_alike(family: &str, first: u32, second: u32) -> Error { Error::new( SqlState::DATATYPE_MISMATCH, format!("arguments declared \"{family}\" are not all alike"), ) + .with_detail(format!("{} versus {}", types::name(first), types::name(second))) } fn mismatch(message: String) -> Error { Error::new(SqlState::DATATYPE_MISMATCH, message) } -/// Reads the actual types of the polymorphic arguments. `Ok(None)` means that the types do not fit, which the check gives as false and the enforcement gives as an error. -fn collect(inputs: &[u32], declared: &[u32]) -> Result { - let mut f = Families::default(); +/// The error of an argument whose type is not of the kind of its family. +fn not_kind(family: &str, kind: &str, ty: u32) -> Error { + mismatch(format!("argument declared {family} is not {kind} but type {}", types::name(ty))) +} + +/// The error of two families that give different types. +fn not_consistent(family: &str, other: &str, first: u32, second: u32) -> Error { + mismatch(format!("argument declared {family} is not consistent with argument declared {other}")) + .with_detail(format!("{} versus {}", types::name(first), types::name(second))) +} + +/// The error of a polymorphic type that only unknown inputs give. +fn undetermined(family: &str) -> Error { + mismatch(format!( + "could not determine polymorphic type {family} because input has type unknown" + )) +} + +/// Keeps the type of an argument of a family, or gives the error of two different types. +fn same(slot: &mut u32, family: &str, ty: u32) -> Result<()> { + if *slot != 0 && *slot != ty { + return Err(not_alike(family, *slot, ty)); + } + *slot = ty; + Ok(()) +} + +/// Reads the actual types of the polymorphic arguments, and the polymorphic result type when it is not 0. An error means that the types do not fit, which the check gives as false and the enforcement gives as the error. +fn collect(inputs: &[u32], declared: &[u32], result: u32) -> Result { + let mut f = Families { + nonarray: result == oid::ANYNONARRAY, + is_enum: result == oid::ANYENUM, + wants_multirange: result == oid::ANYMULTIRANGE, + compatible_nonarray: result == oid::ANYCOMPATIBLENONARRAY, + compatible_array: result == oid::ANYCOMPATIBLEARRAY, + wants_compatible_range: result == oid::ANYCOMPATIBLERANGE, + wants_compatible_multirange: result == oid::ANYCOMPATIBLEMULTIRANGE, + ..Families::default() + }; for (&input, &decl) in inputs.iter().zip(declared) { match decl { oid::ANYELEMENT | oid::ANYNONARRAY | oid::ANYENUM => { f.any_element_args += 1; f.nonarray |= decl == oid::ANYNONARRAY; f.is_enum |= decl == oid::ANYENUM; - if input == oid::UNKNOWN { - f.unknowns = true; - continue; - } - if f.element != 0 && input != f.element { - let detail = - format!("{} versus {}", types::name(f.element), types::name(input)); - return Err(not_alike("anyelement").with_detail(detail)); + if input != oid::UNKNOWN { + same(&mut f.element, "anyelement", input)?; } - f.element = input; } oid::ANYARRAY => { f.any_element_args += 1; - if input == oid::UNKNOWN { - f.unknowns = true; - continue; + if input != oid::UNKNOWN { + same(&mut f.array, "anyarray", types::base(input))?; } - if f.array != 0 && input != f.array { - let detail = format!("{} versus {}", types::name(f.array), types::name(input)); - return Err(not_alike("anyarray").with_detail(detail)); + } + oid::ANYRANGE => { + f.any_element_args += 1; + if input != oid::UNKNOWN { + same(&mut f.range, "anyrange", types::base(input))?; } - f.array = input; } - oid::ANYRANGE - | oid::ANYMULTIRANGE - | oid::ANYCOMPATIBLERANGE - | oid::ANYCOMPATIBLEMULTIRANGE => { + oid::ANYMULTIRANGE => { f.any_element_args += 1; - if input == oid::UNKNOWN { - f.unknowns = true; - continue; + f.wants_multirange = true; + if input != oid::UNKNOWN { + same(&mut f.multirange, "anymultirange", types::base(input))?; } - return Err(Error::new( - SqlState::FEATURE_NOT_SUPPORTED, - format!("range types are not supported yet: {}", types::name(input)), - )); } oid::ANYCOMPATIBLE | oid::ANYCOMPATIBLENONARRAY => { f.has_compatible = true; @@ -96,36 +128,96 @@ fn collect(inputs: &[u32], declared: &[u32]) -> Result { } let element = types::element(types::base(input)); if element == 0 { - return Err(mismatch(format!( - "argument declared anycompatiblearray is not an array but type {}", - types::name(input) - ))); + return Err(not_kind("anycompatiblearray", "an array", types::base(input))); } f.compatible.push(element); } + oid::ANYCOMPATIBLERANGE => { + f.has_compatible = true; + f.wants_compatible_range = true; + if input == oid::UNKNOWN { + continue; + } + let range = types::base(input); + if f.compatible_range != 0 { + same(&mut f.compatible_range, "anycompatiblerange", range)?; + continue; + } + let element = types::range_subtype(range); + if element == 0 { + return Err(not_kind("anycompatiblerange", "a range type", range)); + } + f.compatible_range = range; + f.compatible_range_element = element; + f.compatible.push(element); + } + oid::ANYCOMPATIBLEMULTIRANGE => { + f.has_compatible = true; + f.wants_compatible_multirange = true; + if input == oid::UNKNOWN { + continue; + } + let multirange = types::base(input); + if f.compatible_multirange != 0 { + same(&mut f.compatible_multirange, "anycompatiblemultirange", multirange)?; + continue; + } + if types::multirange_range(multirange) == 0 { + return Err(not_kind( + "anycompatiblemultirange", + "a multirange type", + multirange, + )); + } + f.compatible_multirange = multirange; + } _ => {} } } + if f.any_element_args > 0 { + first_family(&mut f)?; + } + if f.has_compatible { + compatible_ranges(&mut f)?; + } + Ok(f) +} + +/// The checks of the first family: the element type of the array, the range of the multirange and the subtype of the range must be the element type. +fn first_family(f: &mut Families) -> Result<()> { if f.array != 0 { - let element = types::element(types::base(f.array)); + let element = types::element(f.array); + if element == 0 { + return Err(not_kind("anyarray", "an array", f.array)); + } + if f.element == 0 { + f.element = element; + } else if element != f.element { + return Err(not_consistent("anyarray", "anyelement", f.array, f.element)); + } + } + if f.multirange != 0 { + let range = types::multirange_range(f.multirange); + if range == 0 { + return Err(not_kind("anymultirange", "a multirange type", f.multirange)); + } + if f.range == 0 { + f.range = range; + } else if range != f.range { + return Err(not_consistent("anymultirange", "anyrange", f.multirange, f.range)); + } + } else if f.wants_multirange && f.range != 0 { + f.multirange = types::range_multirange(f.range); + } + if f.range != 0 { + let element = types::range_subtype(f.range); if element == 0 { - return Err(mismatch(format!( - "argument declared anyarray is not an array but type {}", - types::name(f.array) - ))); + return Err(not_kind("anyrange", "a range type", f.range)); } if f.element == 0 { f.element = element; } else if element != f.element { - return Err(mismatch( - "argument declared anyarray is not consistent with argument declared anyelement" - .into(), - ) - .with_detail(format!( - "{} versus {}", - types::name(f.array), - types::name(f.element) - ))); + return Err(not_consistent("anyrange", "anyelement", f.range, f.element)); } } if f.nonarray && f.element != 0 && types::element(types::base(f.element)) != 0 { @@ -140,10 +232,43 @@ fn collect(inputs: &[u32], declared: &[u32]) -> Result { types::name(f.element) ))); } - Ok(f) + Ok(()) +} + +/// The range of the `anycompatiblemultirange` argument must be the `anycompatiblerange` type, and its subtype joins the choice of the common type. +fn compatible_ranges(f: &mut Families) -> Result<()> { + if f.compatible_multirange != 0 { + let range = types::multirange_range(f.compatible_multirange); + if f.compatible_range != 0 { + if range != f.compatible_range { + return Err(not_consistent( + "anycompatiblemultirange", + "anycompatiblerange", + f.compatible_multirange, + f.compatible_range, + )); + } + } else { + let element = types::range_subtype(range); + if element == 0 { + return Err(not_kind( + "anycompatiblemultirange", + "a multirange type", + f.compatible_multirange, + )); + } + f.compatible_range = range; + f.compatible_range_element = element; + f.wants_compatible_range = true; + f.compatible.push(element); + } + } else if f.wants_compatible_multirange && f.compatible_range != 0 { + f.compatible_multirange = types::range_multirange(f.compatible_range); + } + Ok(()) } -/// The common type of the `anycompatible` family, or an error if the types have none. +/// The common type of the `anycompatible` family, or an error if the types have none. The subtype of an `anycompatiblerange` argument must be the common type. fn compatible_type(f: &Families) -> Result { if f.compatible.is_empty() { return Ok(oid::TEXT); @@ -160,6 +285,13 @@ fn compatible_type(f: &Families) -> Result { "arguments of anycompatible family cannot be cast to a common type".into(), )); } + if f.compatible_range_element != 0 && f.compatible_range_element != common { + return Err(mismatch(format!( + "anycompatiblerange type {} does not match anycompatible type {}", + types::name(f.compatible_range), + types::name(common) + ))); + } if f.compatible_nonarray && types::element(types::base(common)) != 0 { return Err(mismatch(format!( "type matched to anycompatiblenonarray is an array type: {}", @@ -171,7 +303,7 @@ fn compatible_type(f: &Families) -> Result { /// `check_generic_type_consistency`: true if the actual types fit the polymorphic arguments. pub(crate) fn consistent(inputs: &[u32], declared: &[u32]) -> bool { - let Ok(f) = collect(inputs, declared) else { return false }; + let Ok(f) = collect(inputs, declared, 0) else { return false }; !f.has_compatible || f.compatible.is_empty() || compatible_type(&f).is_ok() } @@ -188,7 +320,10 @@ fn array_of(element: u32) -> Result { /// `enforce_generic_type_consistency`: replaces the polymorphic types in `declared` with the actual types, and gives the actual result type for `result`. pub(crate) fn resolve(inputs: &[u32], declared: &mut [u32], result: u32) -> Result { - let f = collect(inputs, declared)?; + let f = collect(inputs, declared, result)?; + if f.any_element_args == 0 && !f.has_compatible { + return Ok(result); + } let element = f.element; let mut array = f.array; if f.any_element_args > 0 && element == 0 { @@ -199,6 +334,12 @@ pub(crate) fn resolve(inputs: &[u32], declared: &mut [u32], result: u32) -> Resu let (compatible, compatible_array) = if f.has_compatible { let common = compatible_type(&f)?; let array = if f.compatible_array { array_of(common)? } else { 0 }; + if f.wants_compatible_range && f.compatible_range == 0 { + return Err(undetermined("anycompatiblerange")); + } + if f.wants_compatible_multirange && f.compatible_multirange == 0 { + return Err(undetermined("anycompatiblemultirange")); + } (common, array) } else { (0, 0) @@ -207,8 +348,10 @@ pub(crate) fn resolve(inputs: &[u32], declared: &mut [u32], result: u32) -> Resu match *decl { oid::ANYCOMPATIBLE | oid::ANYCOMPATIBLENONARRAY => *decl = compatible, oid::ANYCOMPATIBLEARRAY => *decl = compatible_array, + oid::ANYCOMPATIBLERANGE => *decl = f.compatible_range, + oid::ANYCOMPATIBLEMULTIRANGE => *decl = f.compatible_multirange, oid::ANYELEMENT | oid::ANYNONARRAY | oid::ANYENUM if input == oid::UNKNOWN => { - *decl = element + *decl = element; } oid::ANYARRAY if input == oid::UNKNOWN => { if array == 0 { @@ -216,6 +359,18 @@ pub(crate) fn resolve(inputs: &[u32], declared: &mut [u32], result: u32) -> Resu } *decl = array; } + oid::ANYRANGE if input == oid::UNKNOWN => { + if f.range == 0 { + return Err(undetermined("anyrange")); + } + *decl = f.range; + } + oid::ANYMULTIRANGE if input == oid::UNKNOWN => { + if f.multirange == 0 { + return Err(undetermined("anymultirange")); + } + *decl = f.multirange; + } _ => {} } } @@ -227,14 +382,14 @@ pub(crate) fn resolve(inputs: &[u32], declared: &mut [u32], result: u32) -> Resu } Ok(array) } + oid::ANYRANGE if f.range == 0 => Err(undetermined("anyrange")), + oid::ANYRANGE => Ok(f.range), + oid::ANYMULTIRANGE if f.multirange == 0 => Err(undetermined("anymultirange")), + oid::ANYMULTIRANGE => Ok(f.multirange), oid::ANYCOMPATIBLE | oid::ANYCOMPATIBLENONARRAY => Ok(compatible), oid::ANYCOMPATIBLEARRAY => Ok(compatible_array), - oid::ANYRANGE - | oid::ANYMULTIRANGE - | oid::ANYCOMPATIBLERANGE - | oid::ANYCOMPATIBLEMULTIRANGE => { - Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, "range types are not supported yet")) - } + oid::ANYCOMPATIBLERANGE => Ok(f.compatible_range), + oid::ANYCOMPATIBLEMULTIRANGE => Ok(f.compatible_multirange), _ => Ok(result), } } @@ -267,4 +422,51 @@ mod tests { "could not determine polymorphic type because input has type unknown" ); } + + #[test] + fn ranges() { + assert!(consistent(&[oid::INT4RANGE, oid::INT4], &[oid::ANYRANGE, oid::ANYELEMENT])); + assert!(!consistent(&[oid::INT4RANGE, oid::INT8], &[oid::ANYRANGE, oid::ANYELEMENT])); + assert!(!consistent(&[oid::INT4], &[oid::ANYRANGE])); + assert_eq!( + resolve(&[oid::INT4RANGE], &mut [oid::ANYRANGE], oid::ANYELEMENT).unwrap(), + oid::INT4 + ); + let mut declared = [oid::ANYRANGE, oid::ANYRANGE]; + assert_eq!( + resolve(&[oid::DATERANGE, oid::UNKNOWN], &mut declared, oid::BOOL).unwrap(), + oid::BOOL + ); + assert_eq!(declared, [oid::ANYRANGE, oid::DATERANGE]); + let err = + resolve(&[oid::INT4RANGE, oid::INT8], &mut [oid::ANYRANGE, oid::ANYELEMENT], oid::BOOL) + .unwrap_err(); + assert_eq!( + err.message(), + "argument declared anyrange is not consistent with argument declared anyelement" + ); + let err = + resolve(&[oid::UNKNOWN, oid::INT4], &mut [oid::ANYRANGE, oid::ANYELEMENT], oid::BOOL) + .unwrap_err(); + assert_eq!( + err.message(), + "could not determine polymorphic type anyrange because input has type unknown" + ); + let mut declared = [oid::ANYCOMPATIBLERANGE, oid::ANYCOMPATIBLE]; + assert_eq!( + resolve(&[oid::INT4RANGE, oid::INT4], &mut declared, oid::ANYCOMPATIBLERANGE).unwrap(), + oid::INT4RANGE + ); + assert_eq!(declared, [oid::INT4RANGE, oid::INT4]); + let err = resolve( + &[oid::INT4RANGE, oid::INT8], + &mut [oid::ANYCOMPATIBLERANGE, oid::ANYCOMPATIBLE], + oid::BOOL, + ) + .unwrap_err(); + assert_eq!( + err.message(), + "anycompatiblerange type int4range does not match anycompatible type bigint" + ); + } } diff --git a/crates/rupg-analyze/src/transform.rs b/crates/rupg-analyze/src/transform.rs index 82a33123..ef3937d3 100644 --- a/crates/rupg-analyze/src/transform.rs +++ b/crates/rupg-analyze/src/transform.rs @@ -871,7 +871,7 @@ impl Analyzer<'_> { }) } - /// The function of the `<` operator of the type, for `GREATEST` and `LEAST`. + /// The function of the `<` operator of the type, for `GREATEST` and `LEAST`. A type with no `<` of its own, such as a range type, takes the `<` of its default btree operator class. fn comparison(&self, ty: u32, at: Option) -> Result { let base = types::base(ty); rupg_pgcatalog::builtin::operators_named("<") @@ -879,6 +879,10 @@ impl Analyzer<'_> { op.kind == b'b' && op.left == base && op.right == base && op.namespace == PG_CATALOG }) .map(|op| op.code) + .or_else(|| { + let lt = crate::typcache::operators(base).lt; + rupg_pgcatalog::builtin::operator_by_oid(lt).map(|op| op.code) + }) .ok_or_else(|| { Error::new( SqlState::UNDEFINED_FUNCTION, diff --git a/crates/rupg-analyze/src/types.rs b/crates/rupg-analyze/src/types.rs index b40d8fa5..3aaa3fa9 100644 --- a/crates/rupg-analyze/src/types.rs +++ b/crates/rupg-analyze/src/types.rs @@ -69,6 +69,21 @@ pub fn array_of(ty: u32) -> u32 { row(ty).map_or(0, |t| t.array) } +/// `get_range_subtype`: the subtype of a range type, or 0. +pub fn range_subtype(ty: u32) -> u32 { + builtin::range_subtype(ty).unwrap_or(0) +} + +/// `get_range_multirange`: the multirange type of a range type, or 0. +pub fn range_multirange(ty: u32) -> u32 { + builtin::range_multirange(ty).unwrap_or(0) +} + +/// `get_multirange_range`: the range type of a multirange type, or 0. +pub fn multirange_range(ty: u32) -> u32 { + builtin::multirange_range(ty).unwrap_or(0) +} + /// `type_is_collatable`. pub fn collatable(ty: u32) -> bool { row(base(ty)).is_some_and(|t| t.collation != 0) diff --git a/crates/rupg-func/src/compare.rs b/crates/rupg-func/src/compare.rs index 4c141ac7..4c6e49b0 100644 --- a/crates/rupg-func/src/compare.rs +++ b/crates/rupg-func/src/compare.rs @@ -7,7 +7,7 @@ use rupg_pgcatalog::builtin::{self, ProcRow}; use rupg_types::{Array, RegKind, USECS_PER_DAY, USECS_PER_SEC, Value, oid}; use crate::io::base_type; -use crate::{Call, Kernel, bad_value}; +use crate::{Call, Kernel, bad_value, range}; /// The groups of types that compare with each other. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -41,6 +41,8 @@ enum Family { PgLsn, /// `bit` and `bit varying`, a string of `0` and `1` whose byte order is the order of `bitcmp`. Bit, + /// The range types, which compare as `range_cmp` does. + Range, } /// The family of a type, or `None` for a type that has no comparison here. @@ -73,6 +75,8 @@ fn family(ty: u32) -> Option { oid::MACADDR8 => Family::Macaddr8, oid::PG_LSN => Family::PgLsn, oid::BIT | oid::VARBIT => Family::Bit, + oid::ANYRANGE => Family::Range, + ty if builtin::range_subtype(ty).is_some() => Family::Range, _ => match builtin::type_by_oid(ty) { Some(row) if row.category == b'A' && row.elem != 0 => Family::Array, _ => return None, @@ -129,6 +133,13 @@ pub fn compare(left: u32, a: &Value, right: u32, b: &Value) -> Result } (Value::Interval(x), Value::Interval(y)) => span(x).cmp(&span(y)), (Value::Array(x), Value::Array(y)) => array_order(element(left), x, element(right), y)?, + (Value::Range(x), Value::Range(y)) => { + let sub = match range::subtype(left) { + 0 => range::subtype(right), + sub => sub, + }; + range::range_order(sub, x, y)? + } _ => return Err(bad_value()), }) } @@ -390,7 +401,13 @@ pub(crate) fn by_src(src: &str, proc: &ProcRow) -> Option { } else if src.starts_with("bt") && src.ends_with("cmp") || matches!( src, - "array_cmp" | "cash_cmp" | "xid8cmp" | "macaddr_cmp" | "macaddr8_cmp" | "pg_lsn_cmp" + "array_cmp" + | "cash_cmp" + | "xid8cmp" + | "macaddr_cmp" + | "macaddr8_cmp" + | "pg_lsn_cmp" + | "range_cmp" ) { Some(cmp) diff --git a/crates/rupg-func/src/io.rs b/crates/rupg-func/src/io.rs index b5052fab..f071420c 100644 --- a/crates/rupg-func/src/io.rs +++ b/crates/rupg-func/src/io.rs @@ -45,6 +45,9 @@ pub fn output_supported(ty: u32) -> bool { if let Some((elem, _)) = array_of(ty) { return output_supported(elem); } + if let Some(sub) = builtin::range_subtype(ty) { + return output_supported(sub); + } matches!( ty, oid::BOOL @@ -119,6 +122,9 @@ pub fn input_supported(ty: u32) -> bool { if let Some((elem, _)) = array_of(ty) { return input_supported(elem); } + if let Some(sub) = builtin::range_subtype(ty) { + return input_supported(sub); + } output_supported(ty) && !matches!(ty, oid::ANYARRAY | oid::VOID) && !is_row_type(ty) } @@ -164,6 +170,9 @@ pub fn output(ty: u32, value: &Value, session: &dyn Session, out: &mut Vec) reg::output(kind, *v, session, out); return Ok(()); } + if let Value::Range(range) = value { + return range_out(ty, range, session, out); + } if let Value::Record(record) = value { return record_out(record, session, out); } @@ -244,6 +253,49 @@ pub(crate) fn tid_parts(value: i64) -> Result<(u32, u16)> { Ok((block, offset)) } +/// `range_out`: the output function of the subtype for each bound that the range has, in the text of the range. +fn range_out( + ty: u32, + range: &types::Range, + session: &dyn Session, + out: &mut Vec, +) -> Result<()> { + let sub = crate::range::subtype(ty); + let (mut lower, mut upper) = (Vec::new(), Vec::new()); + if range.has_lower() { + output(sub, &range.lower, session, &mut lower)?; + } + if range.has_upper() { + output(sub, &range.upper, session, &mut upper)?; + } + types::range_deparse( + range.flags, + range.has_lower().then_some(&lower[..]), + range.has_upper().then_some(&upper[..]), + out, + ); + Ok(()) +} + +/// `range_send`: the send function of the subtype for each bound that the range has, in the binary form of the range. +fn range_send(ty: u32, range: &types::Range, out: &mut Vec) -> Result<()> { + let sub = crate::range::subtype(ty); + let (mut lower, mut upper) = (Vec::new(), Vec::new()); + if range.has_lower() { + send(sub, &range.lower, &mut lower)?; + } + if range.has_upper() { + send(sub, &range.upper, &mut upper)?; + } + types::range_send( + range.flags, + range.has_lower().then_some(&lower[..]), + range.has_upper().then_some(&upper[..]), + out, + ); + Ok(()) +} + /// `record_out`: the fields in parentheses with a comma between them. A null field is empty. A field that is empty or has a quote, a backslash, a parenthesis, a comma or white space is in double quotes, with each quote and each backslash doubled. fn record_out(record: &Record, session: &dyn Session, out: &mut Vec) -> Result<()> { out.push(b'('); @@ -404,6 +456,9 @@ fn input_value( (array, None) => Ok(Value::Array(Box::new(array?))), }; } + if let Some(sub) = builtin::range_subtype(ty) { + return range_in(ty, sub, text, session); + } if let Some(kind) = RegKind::from_oid(ty) { return match reg::input(kind, text, session)? { Ok(oid) => Ok(Value::Oid(oid)), @@ -503,6 +558,37 @@ fn input_value( Ok(value) } +/// `range_in`: the text of the range, then the input function of the subtype for each bound that is not infinite. The error of a bound that is above the other bound is a soft error. +fn range_in( + ty: u32, + sub: u32, + text: &str, + session: &dyn Session, +) -> std::result::Result { + let parsed = types::range_parse(text)?; + let bound = |text: Option<&String>| match text { + Some(text) => input_value(sub, text, -1, session), + None => Ok(Value::Null), + }; + let lower = bound(parsed.lower.as_ref())?; + let upper = bound(parsed.upper.as_ref())?; + let range = crate::range::make_range(ty, parsed.flags, lower, upper).map_err(Failed::Soft)?; + Ok(Value::Range(Box::new(range))) +} + +/// `range_recv`: the binary form of the range, then the receive function of the subtype for the bytes of each bound. The receive function of a bound does not have to read all its bytes. +fn range_recv(ty: u32, sub: u32, recv: &mut Recv<'_>) -> std::result::Result { + let (flags, lower, upper) = types::range_recv(recv)?; + let bound = |bytes: Option<&[u8]>| match bytes { + Some(bytes) => receive_from(sub, &mut Recv::new(bytes), -1), + None => Ok(Value::Null), + }; + let lower = bound(lower)?; + let upper = bound(upper)?; + let range = crate::range::make_range(ty, flags, lower, upper).map_err(Received::Engine)?; + Ok(Value::Range(Box::new(range))) +} + /// The error of the input and the receive functions of a type whose values come only from the server. fn no_input(ty: u32) -> Error { Error::new( @@ -576,6 +662,9 @@ fn receive_from(ty: u32, recv: &mut Recv<'_>, typmod: i32) -> std::result::Resul } return Ok(Value::Array(Box::new(array?))); } + if let Some(sub) = builtin::range_subtype(ty) { + return range_recv(ty, sub, recv); + } if let Some(kind) = types::GeoKind::from_oid(ty) { return Ok(Value::Bytea(types::geo_recv(kind, recv)?)); } @@ -710,6 +799,7 @@ pub fn send(ty: u32, value: &Value, out: &mut Vec) -> Result<()> { types::oidvector_send(&values, out); } (_, Value::Record(record)) => record_send(record, out)?, + (_, Value::Range(range)) => range_send(ty, range, 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/lib.rs b/crates/rupg-func/src/lib.rs index 0fc60f3e..4d6f9015 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -24,6 +24,7 @@ mod mac; mod math; mod misc; mod network; +mod range; mod reg; mod regex; mod replication; @@ -275,6 +276,7 @@ fn by_src(src: &str) -> Option { .or_else(|| array::by_src(src)) .or_else(|| regex::by_src(src)) .or_else(|| network::by_src(src)) + .or_else(|| range::by_src(src)) .or_else(|| activity::by_src(src)) .or_else(|| replication::by_src(src)) .or_else(|| shared::by_src(src)) diff --git a/crates/rupg-func/src/math.rs b/crates/rupg-func/src/math.rs index 11523d90..ef739e3c 100644 --- a/crates/rupg-func/src/math.rs +++ b/crates/rupg-func/src/math.rs @@ -532,7 +532,7 @@ fn numeric_sign(_: &Call<'_>, args: &[Value]) -> Result { } /// `IS_VALID_DATE`. -fn valid_date(d: i64) -> bool { +pub(crate) fn valid_date(d: i64) -> bool { (-i64::from(POSTGRES_EPOCH_JDATE)..i64::from(DATE_END_JULIAN - POSTGRES_EPOCH_JDATE)) .contains(&d) } diff --git a/crates/rupg-func/src/range.rs b/crates/rupg-func/src/range.rs new file mode 100644 index 00000000..d8d1b7b0 --- /dev/null +++ b/crates/rupg-func/src/range.rs @@ -0,0 +1,319 @@ +//! The range types of `rangetypes.c`: `make_range` with the canonical functions of the discrete ranges, the order of the ranges, the constructors and the functions that read the bounds and the flags. + +use std::cmp::Ordering; + +use rupg_common::{Error, Result, SqlState}; +use rupg_pgcatalog::builtin; +use rupg_types::{ + DATE_INFINITY, DATE_NEGATIVE_INFINITY, RANGE_EMPTY, RANGE_LB_INC, RANGE_LB_INF, RANGE_UB_INC, + RANGE_UB_INF, Range, Value, oid, +}; + +use crate::compare::compare; +use crate::io::base_type; +use crate::{Call, Kernel, bad_value}; + +/// The subtype of a range type, or 0 for a type that is not a range type. +pub(crate) fn subtype(range: u32) -> u32 { + builtin::range_subtype(base_type(range)).unwrap_or(0) +} + +/// One bound of a range, as `RangeBound` of `rangetypes.h` gives it. +struct Bound<'a> { + value: &'a Value, + infinite: bool, + inclusive: bool, + lower: bool, +} + +/// The lower bound and the upper bound of a range. +fn bounds(range: &Range) -> (Bound<'_>, Bound<'_>) { + let flags = range.flags; + ( + Bound { + value: &range.lower, + infinite: flags & RANGE_LB_INF != 0, + inclusive: flags & RANGE_LB_INC != 0, + lower: true, + }, + Bound { + value: &range.upper, + infinite: flags & RANGE_UB_INF != 0, + inclusive: flags & RANGE_UB_INC != 0, + lower: false, + }, + ) +} + +/// The side of an infinite bound: a lower bound is below each value and an upper bound is above. +fn side(lower: bool) -> Ordering { + if lower { Ordering::Less } else { Ordering::Greater } +} + +/// `range_cmp_bound_values`: the order of the values of two bounds, with an infinite bound below or above each value. The inclusive flags have no effect. +fn cmp_bound_values(sub: u32, b1: &Bound<'_>, b2: &Bound<'_>) -> Result { + Ok(match (b1.infinite, b2.infinite) { + (true, true) if b1.lower == b2.lower => Ordering::Equal, + (true, _) => side(b1.lower), + (false, true) => side(b2.lower).reverse(), + (false, false) => compare(sub, b1.value, sub, b2.value)?, + }) +} + +/// `range_cmp_bounds`: the order of two bounds. For equal values, an exclusive lower bound is after the value and an exclusive upper bound is before it. +fn cmp_bounds(sub: u32, b1: &Bound<'_>, b2: &Bound<'_>) -> Result { + let order = cmp_bound_values(sub, b1, b2)?; + if order.is_ne() || b1.infinite || b2.infinite { + return Ok(order); + } + Ok(match (b1.inclusive, b2.inclusive) { + (false, false) if b1.lower == b2.lower => Ordering::Equal, + (false, _) => side(b1.lower).reverse(), + (true, false) => side(b2.lower), + (true, true) => Ordering::Equal, + }) +} + +/// `range_cmp`: an empty range is first, then the order of the lower bounds, then the order of the upper bounds. `sub` is the subtype. +pub(crate) fn range_order(sub: u32, a: &Range, b: &Range) -> Result { + match (a.is_empty(), b.is_empty()) { + (true, true) => return Ok(Ordering::Equal), + (true, false) => return Ok(Ordering::Less), + (false, true) => return Ok(Ordering::Greater), + (false, false) => {} + } + let (lower1, upper1) = bounds(a); + let (lower2, upper2) = bounds(b); + Ok(cmp_bounds(sub, &lower1, &lower2)?.then(cmp_bounds(sub, &upper1, &upper2)?)) +} + +/// `range_serialize`: an infinite bound has no value and is not inclusive. Two equal values give an empty range when a bound is exclusive. +fn serialize(sub: u32, flags: u8, lower: Value, upper: Value) -> Result { + if flags & RANGE_EMPTY != 0 { + return Ok(Range::empty()); + } + let mut flags = flags & (RANGE_LB_INC | RANGE_UB_INC | RANGE_LB_INF | RANGE_UB_INF); + let lower = if flags & RANGE_LB_INF != 0 { + flags &= !RANGE_LB_INC; + Value::Null + } else { + lower + }; + let upper = if flags & RANGE_UB_INF != 0 { + flags &= !RANGE_UB_INC; + Value::Null + } else { + upper + }; + if flags & (RANGE_LB_INF | RANGE_UB_INF) == 0 { + match compare(sub, &lower, sub, &upper)? { + Ordering::Greater => { + return Err(Error::new( + SqlState::DATA_EXCEPTION, + "range lower bound must be less than or equal to range upper bound", + )); + } + Ordering::Equal + if flags & (RANGE_LB_INC | RANGE_UB_INC) != RANGE_LB_INC | RANGE_UB_INC => + { + return Ok(Range::empty()); + } + _ => {} + } + } + Ok(Range { flags, lower, upper }) +} + +/// The next value of a discrete subtype: `int4range_canonical`, `int8range_canonical` and `daterange_canonical` add 1. An infinite date does not change. +fn next(value: &Value) -> Result { + let out_of_range = + |message: &str| Error::new(SqlState::NUMERIC_VALUE_OUT_OF_RANGE, message.to_string()); + match value { + Value::Int4(v) => { + v.checked_add(1).map(Value::Int4).ok_or_else(|| out_of_range("integer out of range")) + } + Value::Int8(v) => { + v.checked_add(1).map(Value::Int8).ok_or_else(|| out_of_range("bigint out of range")) + } + Value::Date(v) if *v == DATE_INFINITY || *v == DATE_NEGATIVE_INFINITY => { + Ok(Value::Date(*v)) + } + Value::Date(v) => match v.checked_add(1) { + Some(d) if crate::math::valid_date(i64::from(d)) => Ok(Value::Date(d)), + _ => Err(Error::new(SqlState::DATETIME_VALUE_OUT_OF_RANGE, "date out of range")), + }, + _ => Err(bad_value()), + } +} + +/// True for a value that the canonical function of a date range leaves as it is. +fn infinite_date(value: &Value) -> bool { + matches!(value, Value::Date(v) if *v == DATE_INFINITY || *v == DATE_NEGATIVE_INFINITY) +} + +/// `make_range`: the range of the type `range` from the flags and the bounds, with the canonical form of `int4range`, `int8range` and `daterange`, which have an inclusive lower bound and an exclusive upper bound. +/// +/// # Errors +/// +/// `22000` when the lower bound is above the upper bound, and the error of a bound that has no next value. +pub(crate) fn make_range(range: u32, flags: u8, lower: Value, upper: Value) -> Result { + let range_ty = base_type(range); + let sub = subtype(range_ty); + let mut out = serialize(sub, flags, lower, upper)?; + if out.is_empty() || !matches!(range_ty, oid::INT4RANGE | oid::INT8RANGE | oid::DATERANGE) { + return Ok(out); + } + let mut flags = out.flags; + if out.has_lower() && flags & RANGE_LB_INC == 0 && !infinite_date(&out.lower) { + out.lower = next(&out.lower)?; + flags |= RANGE_LB_INC; + } + if out.has_upper() && flags & RANGE_UB_INC != 0 && !infinite_date(&out.upper) { + out.upper = next(&out.upper)?; + flags &= !RANGE_UB_INC; + } + serialize(sub, flags, out.lower, out.upper) +} + +/// The range of a call. +fn arg(args: &[Value]) -> Result<&Range> { + match args.first() { + Some(Value::Range(range)) => Ok(range), + _ => Err(bad_value()), + } +} + +/// `range_parse_flags`: the flags of the text `[]`, `[)`, `(]` or `()`. +fn parse_flags(text: &str) -> Result { + let b = text.as_bytes(); + let (lower, upper) = match b { + [l @ (b'[' | b'('), u @ (b']' | b')')] => (*l, *u), + _ => { + return Err(Error::new(SqlState::SYNTAX_ERROR, "invalid range bound flags") + .with_hint("Valid values are \"[]\", \"[)\", \"(]\", and \"()\".")); + } + }; + Ok(if lower == b'[' { RANGE_LB_INC } else { 0 } | if upper == b']' { RANGE_UB_INC } else { 0 }) +} + +/// The flags of a bound that is null, which is infinite. +fn infinite(value: &Value, flag: u8) -> u8 { + if value.is_null() { flag } else { 0 } +} + +/// `range_constructor2`: a range with an inclusive lower bound and an exclusive upper bound. A null bound is infinite. +fn constructor2(call: &Call<'_>, args: &[Value]) -> Result { + let [lower, upper] = args else { return Err(bad_value()) }; + let flags = RANGE_LB_INC | infinite(lower, RANGE_LB_INF) | infinite(upper, RANGE_UB_INF); + Ok(Value::Range(Box::new(make_range(call.ret, flags, lower.clone(), upper.clone())?))) +} + +/// `range_constructor3`: a range with the bound flags of the third argument. +fn constructor3(call: &Call<'_>, args: &[Value]) -> Result { + let [lower, upper, text] = args else { return Err(bad_value()) }; + let text = match text { + Value::Text(text) => text, + Value::Null => { + return Err(Error::new( + SqlState::DATA_EXCEPTION, + "range constructor flags argument must not be null", + )); + } + _ => return Err(bad_value()), + }; + let flags = parse_flags(text)? | infinite(lower, RANGE_LB_INF) | infinite(upper, RANGE_UB_INF); + Ok(Value::Range(Box::new(make_range(call.ret, flags, lower.clone(), upper.clone())?))) +} + +/// `range_lower`: the lower bound, or null for an empty range or an infinite bound. +fn lower(_: &Call<'_>, args: &[Value]) -> Result { + let range = arg(args)?; + Ok(if range.has_lower() { range.lower.clone() } else { Value::Null }) +} + +/// `range_upper`: the upper bound, or null for an empty range or an infinite bound. +fn upper(_: &Call<'_>, args: &[Value]) -> Result { + let range = arg(args)?; + Ok(if range.has_upper() { range.upper.clone() } else { Value::Null }) +} + +/// The kernel of a function that tests a flag of the range. +macro_rules! flag_test { + ($name:ident, $flag:expr, $doc:literal) => { + #[doc = $doc] + fn $name(_: &Call<'_>, args: &[Value]) -> Result { + Ok(Value::Bool(arg(args)?.flags & $flag != 0)) + } + }; +} + +flag_test!(is_empty, RANGE_EMPTY, "`range_empty`: true for an empty range."); +flag_test!( + lower_inc, + RANGE_LB_INC, + "`range_lower_inc`: true when the lower bound is part of the range." +); +flag_test!( + upper_inc, + RANGE_UB_INC, + "`range_upper_inc`: true when the upper bound is part of the range." +); +flag_test!(lower_inf, RANGE_LB_INF, "`range_lower_inf`: true when the range has no lower bound."); +flag_test!(upper_inf, RANGE_UB_INF, "`range_upper_inf`: true when the range has no upper bound."); + +/// The kernel of a range function by its `prosrc`. +pub(crate) fn by_src(src: &str) -> Option { + Some(match src { + "range_constructor2" => constructor2, + "range_constructor3" => constructor3, + "range_lower" => lower, + "range_upper" => upper, + "range_empty" => is_empty, + "range_lower_inc" => lower_inc, + "range_upper_inc" => upper_inc, + "range_lower_inf" => lower_inf, + "range_upper_inf" => upper_inf, + _ => return None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn int4(flags: u8, lower: i32, upper: i32) -> Result { + make_range(oid::INT4RANGE, flags, Value::Int4(lower), Value::Int4(upper)) + } + + #[test] + fn canonical() { + let range = int4(RANGE_UB_INC, 1, 10).unwrap(); + assert_eq!( + (range.flags, range.lower, range.upper), + (RANGE_LB_INC, Value::Int4(2), Value::Int4(11)) + ); + assert!(int4(0, 5, 6).unwrap().is_empty()); + assert!(int4(RANGE_LB_INC, 5, 5).unwrap().is_empty()); + assert!(!int4(RANGE_LB_INC | RANGE_UB_INC, 5, 5).unwrap().is_empty()); + assert_eq!(int4(RANGE_LB_INC, 10, 1).unwrap_err().state(), SqlState::DATA_EXCEPTION); + assert_eq!(int4(RANGE_UB_INC, 1, i32::MAX).unwrap_err().message(), "integer out of range"); + let range = + make_range(oid::INT4RANGE, RANGE_LB_INF | RANGE_LB_INC, Value::Null, Value::Int4(3)); + assert_eq!(range.unwrap().flags, RANGE_LB_INF); + } + + #[test] + fn order() { + let a = int4(RANGE_LB_INC, 1, 10).unwrap(); + let b = int4(RANGE_LB_INC | RANGE_UB_INC, 1, 9).unwrap(); + assert_eq!(range_order(oid::INT4, &a, &b).unwrap(), Ordering::Equal); + let empty = Range::empty(); + assert_eq!(range_order(oid::INT4, &empty, &a).unwrap(), Ordering::Less); + let all = + Range { flags: RANGE_LB_INF | RANGE_UB_INF, lower: Value::Null, upper: Value::Null }; + assert_eq!(range_order(oid::INT4, &all, &a).unwrap(), Ordering::Less); + let open = + Range { flags: RANGE_LB_INC | RANGE_UB_INF, lower: Value::Int4(1), upper: Value::Null }; + assert_eq!(range_order(oid::INT4, &open, &a).unwrap(), Ordering::Greater); + } +} diff --git a/crates/rupg-pgcatalog/src/builtin.rs b/crates/rupg-pgcatalog/src/builtin.rs index 8dae2678..10f98525 100644 --- a/crates/rupg-pgcatalog/src/builtin.rs +++ b/crates/rupg-pgcatalog/src/builtin.rs @@ -389,6 +389,8 @@ struct Builtin { roles: Vec, members: Vec, multirange_range: HashMap, + /// The subtype and the multirange type of each range type. + range_types: HashMap, } static BUILTIN: LazyLock = LazyLock::new(Builtin::load); @@ -530,6 +532,7 @@ impl Builtin { roles: load_roles(), members: load_members(), multirange_range: load_multirange_ranges(), + range_types: load_range_types(), types, procs, operators, @@ -842,6 +845,14 @@ fn load_multirange_ranges() -> HashMap { (0..r.catalog.len).map(|i| (multirange[i], range[i])).collect() } +/// The subtype and the multirange type of each range type, from `pg_range`. +fn load_range_types() -> HashMap { + let r = Rows::new("pg_range"); + let (range, subtype, multirange) = + (r.oid("rngtypid"), r.oid("rngsubtype"), r.oid("rngmultitypid")); + (0..r.catalog.len).map(|i| (range[i], (subtype[i], multirange[i]))).collect() +} + /// The values of an `aclitem[]` column, for [`Rows::optional`]. fn text_array(values: &'static Values) -> Option<&'static [&'static [&'static str]]> { match values { @@ -1168,6 +1179,16 @@ pub fn multirange_range(multirange: u32) -> Option { BUILTIN.multirange_range.get(&multirange).copied() } +/// The subtype of a range type, as `get_range_subtype` gives it. +pub fn range_subtype(range: u32) -> Option { + BUILTIN.range_types.get(&range).map(|&(subtype, _)| subtype) +} + +/// The multirange type of a range type, as `get_range_multirange` gives it. +pub fn range_multirange(range: u32) -> Option { + BUILTIN.range_types.get(&range).map(|&(_, multirange)| multirange) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index 12f0aebb..0c1ed03d 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -1832,6 +1832,12 @@ mod tests { big_stack(|| script(include_str!("connection/mac.test"))); } + /// The range types: the text forms with the canonical form of `int4range`, `int8range` and `daterange`, the errors of a bad literal and of a lower bound above the upper bound, the soft errors, the constructors, the functions of the bounds and the flags, the order, the arrays of ranges, a parameter and the text and binary forms of `COPY`. `connection/range.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn range() { + big_stack(|| script(include_str!("connection/range.test"))); + } + /// `inet` and `cidr`: the text forms of IPv4 and IPv6 with the errors of a bad value, the class of a `cidr` with no mask length, the casts, the functions such as `abbrev` and `set_masklen`, the containment, bit and arithmetic operators and the order. `connection/inet.test` is the output of PostgreSQL 19 for the same script in a new database. #[test] fn inet() { diff --git a/crates/rupg-session/src/connection/range.test b/crates/rupg-session/src/connection/range.test new file mode 100644 index 00000000..fc6225bb --- /dev/null +++ b/crates/rupg-session/src/connection/range.test @@ -0,0 +1,234 @@ +> SET TimeZone = 'UTC' +SET +> SET DateStyle = 'ISO, MDY' +SET +> SELECT '[1,10)'::int4range, '[1,10]'::int4range, '(1,10]'::int4range, '(1,10)'::int4range, '(,10)'::int4range, '[1,)'::int4range, '(,)'::int4range +fields int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1 +row [1,10),[1,11),[2,11),[2,10),(,10),[1,),(,) +SELECT 1 +> SELECT 'empty'::int4range, ' EMPTY '::int4range, '[5,5)'::int4range, '(5,5]'::int4range, '[5,5]'::int4range, '(5,6)'::int4range +fields int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1 +row empty,empty,empty,empty,[5,6),empty +SELECT 1 +> SELECT '[,10]'::int4range, '[1,]'::int4range, '[ 1 , 10 ]'::int4range, ' [1,10) '::int4range +fields int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1 +row (,11),[1,),[1,11),[1,10) +SELECT 1 +> SELECT '["1","10")'::int4range, '[1,"10"]'::int4range +fields int4range:3904:-1:-1,int4range:3904:-1:-1 +row [1,10),[1,11) +SELECT 1 +> SELECT '[10,1)'::int4range +ERROR 22000 range lower bound must be less than or equal to range upper bound at 8 +> SELECT '[1,10'::int4range +ERROR 22P02 malformed range literal: "[1,10" at 8 +> SELECT '1,10)'::int4range +ERROR 22P02 malformed range literal: "1,10)" at 8 +> SELECT '[1 10)'::int4range +ERROR 22P02 malformed range literal: "[1 10)" at 8 +> SELECT '[1,10,11)'::int4range +ERROR 22P02 malformed range literal: "[1,10,11)" at 8 +> SELECT '[1,10)x'::int4range +ERROR 22P02 malformed range literal: "[1,10)x" at 8 +> SELECT ''::int4range +ERROR 22P02 malformed range literal: "" at 8 +> SELECT 'emptyx'::int4range +ERROR 22P02 malformed range literal: "emptyx" at 8 +> SELECT '[a,10)'::int4range +ERROR 22P02 invalid input syntax for type integer: "a" at 8 +> SELECT '[1,2147483647]'::int4range +ERROR 22003 integer out of range at 8 +> SELECT '[1,2147483647)'::int4range, '(2147483646,2147483647]'::int4range +ERROR 22003 integer out of range at 37 +> SELECT '(-2147483648,5)'::int4range, '[-2147483648,5)'::int4range +fields int4range:3904:-1:-1,int4range:3904:-1:-1 +row [-2147483647,5),[-2147483648,5) +SELECT 1 +> SELECT '[1,10]'::int8range, '(1,10)'::int8range, '[1,9223372036854775807)'::int8range +fields int8range:3926:-1:-1,int8range:3926:-1:-1,int8range:3926:-1:-1 +row [1,11),[2,10),[1,9223372036854775807) +SELECT 1 +> SELECT '[1,9223372036854775807]'::int8range +ERROR 22003 bigint out of range at 8 +> SELECT '[1.5,10)'::numrange, '(1.50,10.0]'::numrange, '[1,1]'::numrange, '(1,1]'::numrange, '[-inf,inf]'::numrange +fields numrange:3906:-1:-1,numrange:3906:-1:-1,numrange:3906:-1:-1,numrange:3906:-1:-1,numrange:3906:-1:-1 +row [1.5,10),(1.50,10.0],[1,1],empty,[-Infinity,Infinity] +SELECT 1 +> SELECT '[NaN,1)'::numrange +ERROR 22000 range lower bound must be less than or equal to range upper bound at 8 +> SELECT '[1,NaN)'::numrange +fields numrange:3906:-1:-1 +row [1,NaN) +SELECT 1 +> SELECT '[2020-01-01,2020-01-05]'::daterange, '(2020-01-01,2020-01-05)'::daterange, '[-infinity,infinity]'::daterange, '(infinity,infinity]'::daterange +fields daterange:3912:-1:-1,daterange:3912:-1:-1,daterange:3912:-1:-1,daterange:3912:-1:-1 +row [2020-01-01,2020-01-06),[2020-01-02,2020-01-05),[-infinity,infinity],empty +SELECT 1 +> SELECT '[2020-01-01 10:00,2020-01-05)'::tsrange, '["2020-01-01 10:00","2020-01-05 00:00:01.5"]'::tsrange, '[-infinity,infinity)'::tsrange +fields tsrange:3908:-1:-1,tsrange:3908:-1:-1,tsrange:3908:-1:-1 +row ["2020-01-01 10:00:00","2020-01-05 00:00:00"),["2020-01-01 10:00:00","2020-01-05 00:00:01.5"],[-infinity,infinity) +SELECT 1 +> SELECT '[2020-01-01 10:00+02,2020-01-05)'::tstzrange +fields tstzrange:3910:-1:-1 +row ["2020-01-01 08:00:00+00","2020-01-05 00:00:00+00") +SELECT 1 +> SELECT int4range(1, 10), int4range(1, 10, '[]'), int4range(1, 10, '()'), int4range(1, 10, '(]'), int4range(NULL, 10), int4range(1, NULL, '[]'), int4range(NULL, NULL) +fields int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1,int4range:3904:-1:-1 +row [1,10),[1,11),[2,10),[2,11),(,10),[1,),(,) +SELECT 1 +> SELECT int4range(1, 10, 'x') +ERROR 42601 invalid range bound flags +> SELECT int4range(1, 10, NULL) +ERROR 22000 range constructor flags argument must not be null +> SELECT int4range(10, 1) +ERROR 22000 range lower bound must be less than or equal to range upper bound +> SELECT int8range(1, 10), numrange(1.5, 2.5), numrange(1, 2, '[]'), daterange('2020-01-01', '2020-02-01', '[]'), tsrange('2020-01-01', '2020-01-02'), tstzrange('2020-01-01', '2020-01-02') +fields int8range:3926:-1:-1,numrange:3906:-1:-1,numrange:3906:-1:-1,daterange:3912:-1:-1,tsrange:3908:-1:-1,tstzrange:3910:-1:-1 +row [1,10),[1.5,2.5),[1,2],[2020-01-01,2020-02-02),["2020-01-01 00:00:00","2020-01-02 00:00:00"),["2020-01-01 00:00:00+00","2020-01-02 00:00:00+00") +SELECT 1 +> SELECT numrange(1, 'NaN') +fields numrange:3906:-1:-1 +row [1,NaN) +SELECT 1 +> SELECT lower('[1,10)'::int4range), upper('[1,10)'::int4range), lower('(,10)'::int4range), upper('empty'::int4range), lower('[1.5,2]'::numrange) +fields lower:23:4:-1,upper:23:4:-1,lower:23:4:-1,upper:23:4:-1,lower:1700:-1:-1 +row 1,10,NULL,NULL,1.5 +SELECT 1 +> SELECT isempty('empty'::int4range), isempty('[1,2)'::int4range), lower_inc('[1,2)'::int4range), upper_inc('[1,2)'::int4range), lower_inf('(,2)'::int4range), upper_inf('(,2)'::int4range), lower_inc('empty'::int4range), lower_inf('empty'::int4range) +fields isempty:16:1:-1,isempty:16:1:-1,lower_inc:16:1:-1,upper_inc:16:1:-1,lower_inf:16:1:-1,upper_inf:16:1:-1,lower_inc:16:1:-1,lower_inf:16:1:-1 +row t,f,t,f,t,f,f,f +SELECT 1 +> SELECT lower_inc('[1,2]'::numrange), upper_inc('[1,2]'::numrange), upper_inc('(,)'::numrange), lower_inc('(,)'::numrange) +fields lower_inc:16:1:-1,upper_inc:16:1:-1,upper_inc:16:1:-1,lower_inc:16:1:-1 +row t,t,f,f +SELECT 1 +> SELECT pg_typeof(lower('[1,10)'::int8range)), pg_typeof(int4range(1,2)), pg_typeof('[2020-01-01,2020-01-02]'::daterange), pg_typeof(lower('[2020-01-01,2020-01-05]'::daterange)) +fields pg_typeof:2206:4:-1,pg_typeof:2206:4:-1,pg_typeof:2206:4:-1,pg_typeof:2206:4:-1 +row bigint,int4range,daterange,date +SELECT 1 +> SELECT r FROM (VALUES ('[1,10)'::int4range), ('empty'), ('(,5)'), ('[1,5)'), ('[1,)'), ('(,)'), ('[0,3)'), ('[1,10]')) v(r) ORDER BY r +fields r:3904:-1:-1 +row empty +row (,5) +row (,) +row [0,3) +row [1,5) +row [1,10) +row [1,11) +row [1,) +SELECT 8 +> SELECT '[1,10)'::int4range = '[1,9]'::int4range, '[1,10)'::int4range < '[1,11)'::int4range, 'empty'::int4range < '(,)'::int4range, '(,1)'::int4range < '[0,1)'::int4range, '[1,)'::int4range > '[1,5)'::int4range, '[1,2)'::int4range <> '[1,3)'::int4range, '[1,2)'::int4range <= '[1,2)'::int4range, '[1,2)'::int4range >= '[1,3)'::int4range +fields ?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1 +row t,t,t,t,t,t,t,f +SELECT 1 +> SELECT range_cmp('[1,10)'::int4range, '[1,9]'::int4range), range_cmp('empty'::int4range, '[1,2)'::int4range), range_cmp('[1,2)'::int4range, 'empty'::int4range), range_cmp('(,5)'::int4range, '[1,5)'::int4range) +fields range_cmp:23:4:-1,range_cmp:23:4:-1,range_cmp:23:4:-1,range_cmp:23:4:-1 +row 0,-1,1,-1 +SELECT 1 +> SELECT '[1.0,2.0]'::numrange = '[1,2]'::numrange, '[1,2)'::numrange < '[1,2]'::numrange, '(1,2)'::numrange > '[1,2)'::numrange +fields ?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1 +row t,t,t +SELECT 1 +> SELECT ARRAY['[1,2)'::int4range, 'empty', '(,)'], '{"[1,3)","(4,7]"}'::int4range[], '{"[a,b)"}'::text[] +fields array:3905:-1:-1,int4range:3905:-1:-1,text:1009:-1:-1 +row {"[1,2)",empty,"(,)"},{"[1,3)","[5,8)"},{"[a,b)"} +SELECT 1 +> SELECT greatest('[1,2)'::int4range, '[1,3)'::int4range), least('[1,2)'::int4range, 'empty'::int4range) +fields greatest:3904:-1:-1,least:3904:-1:-1 +row [1,3),empty +SELECT 1 +> SELECT '[1,10)'::int4range::text, '(1,10]'::text::int4range, CAST('[2020-01-01,2020-01-02]' AS daterange) +fields text:25:-1:-1,int4range:3904:-1:-1,daterange:3912:-1:-1 +row [1,10),[2,11),[2020-01-01,2020-01-03) +SELECT 1 +> SELECT pg_input_is_valid('[1,2)', 'int4range'), pg_input_is_valid('[2,1)', 'int4range'), pg_input_is_valid('[a,1)', 'int4range'), pg_input_is_valid('[1,2', 'int4range') +fields pg_input_is_valid:16:1:-1,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,f +SELECT 1 +> SELECT * FROM pg_input_error_info('[2,1)', 'int4range') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row range lower bound must be less than or equal to range upper bound,NULL,NULL,22000 +SELECT 1 +> SELECT * FROM pg_input_error_info('[a,1)', 'int4range') +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 integer: "a",NULL,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('[1,2', 'int4range') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row malformed range literal: "[1,2",Unexpected end of input.,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('[1,2147483647]', 'int4range') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row integer out of range,NULL,NULL,22003 +SELECT 1 +> SELECT '("a b","c\\d"]'::tsrange +ERROR 22007 invalid input syntax for type timestamp: "a b" at 8 +> SELECT '[a\,b,c]'::int4range +ERROR 22P02 invalid input syntax for type integer: "a,b" at 8 +> SELECT '[1,2)'::int4range::int8range +ERROR 42846 cannot cast type int4range to int8range at 26 +> SELECT lower(NULL::int4range), lower('[1,2)') +fields lower:23:4:-1,lower:25:-1:-1 +row NULL,[1,2) +SELECT 1 +> SELECT '[1,2)'::int4range IS NULL, '[1,2)'::int4range IS DISTINCT FROM '[1,3)'::int4range +fields ?column?:16:1:-1,?column?:16:1:-1 +row f,t +SELECT 1 +> SELECT DISTINCT r FROM (VALUES ('[1,10)'::int4range), ('[1,9]'), ('empty'), ('[5,5)')) v(r) ORDER BY r +fields r:3904:-1:-1 +row empty +row [1,10) +SELECT 2 +> COPY (SELECT '[1,10)'::int4range, 'empty'::int4range, '(,5]'::int8range, '[1.5,2]'::numrange, '[2020-01-01,2020-01-05]'::daterange, '[2020-01-01 10:00,)'::tsrange, '(,)'::tstzrange) TO stdout +copy out 0 0,0,0,0,0,0,0 +data [1,10)\tempty\t(,6)\t[1.5,2]\t[2020-01-01,2020-01-06)\t[\"2020-01-01 10:00:00\",)\t(,)\n +copy done +COPY 1 +> COPY (SELECT '[1,10)'::int4range, 'empty'::int4range, '(,5]'::int8range, '[1.5,2]'::numrange, '[2020-01-01,2020-01-05]'::daterange, '[2020-01-01 10:00,)'::tsrange, '(,)'::tstzrange) TO stdout (FORMAT binary) +copy out 1 1,1,1,1,1,1,1 +data PGCOPY\n\xff\r\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x11\x02\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\n\x00\x00\x00\x01\x01\x00\x00\x00\r\x08\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x1f\x06\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x01\x00\x01\x13\x88\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x11\x02\x00\x00\x00\x04\x00\x00\x1c\x89\x00\x00\x00\x04\x00\x00\x1c\x8e\x00\x00\x00\r\x12\x00\x00\x00\x08\x00\x02>\x0f\xe8\x86\xc8\x00\x00\x00\x00\x01\x18 +data \xff\xff +copy done +COPY 1 +> SELECT int4range(1, 2) = int4range(1, 2, '[)'), lower(int4range(NULL, 5)), upper_inf(int4range(1, NULL)) +fields ?column?:16:1:-1,lower:23:4:-1,upper_inf:16:1:-1 +row t,NULL,t +SELECT 1 +> SELECT numrange(1.5, 2.5, '[]') > numrange(1.5, 2.5), daterange('2020-01-01', '2020-01-01', '[]') +fields ?column?:16:1:-1,daterange:3912:-1:-1 +row t,[2020-01-01,2020-01-02) +SELECT 1 +> SELECT '[1,2)'::int4range = '[1,2)'::int8range +ERROR 42883 operator does not exist: int4range = int8range at 27 +> SELECT lower(int4range(1, 2), 1) +ERROR 42883 function lower(int4range, integer) does not exist at 8 +> SELECT int4range(1, 2) < '[1,3)', '[1,3)' > int4range(1, 2) +fields ?column?:16:1:-1,?column?:16:1:-1 +row t,t +SELECT 1 +P q1|SELECT $1::int4range, lower($1::int4range) +X q1|[3,7] +row [3,8),3 +SELECT 1 +X q1|empty +row empty,NULL +SELECT 1 +> SELECT '(2020-01-01,infinity]'::daterange, '[-infinity,2020-01-01]'::daterange +fields daterange:3912:-1:-1,daterange:3912:-1:-1 +row [2020-01-02,infinity],[-infinity,2020-01-02) +SELECT 1 +> SELECT '[1,10)'::int4range IN ('[1,9]', 'empty'), CASE WHEN '[1,2)'::int4range = 'empty' THEN 1 ELSE 2 END +fields ?column?:16:1:-1,case:23:4:-1 +row t,2 +SELECT 1 +> SELECT r, count(*) FROM (VALUES ('[1,10)'::int4range), ('[1,9]'), ('empty'), ('[5,5)'), ('(,3)')) v(r) GROUP BY r ORDER BY r +fields r:3904:-1:-1,count:20:8:-1 +row empty,2 +row (,3),1 +row [1,10),2 +SELECT 3 +> SELECT a.r FROM (VALUES ('[1,3)'::int4range), ('[2,4)')) a(r) JOIN (VALUES ('[1,2]'::int4range)) b(r) ON a.r = b.r +fields r:3904:-1:-1 +row [1,3) +SELECT 1 diff --git a/crates/rupg-types/src/lib.rs b/crates/rupg-types/src/lib.rs index 88ad851e..72c5882b 100644 --- a/crates/rupg-types/src/lib.rs +++ b/crates/rupg-types/src/lib.rs @@ -64,6 +64,7 @@ mod network; mod number; mod numeric; pub mod numeric_layout; +mod range; mod reg; mod scalar; mod snapshot; @@ -118,6 +119,10 @@ pub use numeric::{ Numeric, NumericSign, decimal_out, decimal_send, numeric_in, numeric_out, numeric_recv, numeric_send, }; +pub use range::{ + RANGE_EMPTY, RANGE_LB_INC, RANGE_LB_INF, RANGE_UB_INC, RANGE_UB_INF, Range, RangeBytes, + RangeText, range_deparse, range_parse, range_recv, range_send, +}; pub use reg::{ RegInput, RegKind, qualified_name_list, reg_in, reg_out_oid, split_identifier_string, }; diff --git a/crates/rupg-types/src/range.rs b/crates/rupg-types/src/range.rs new file mode 100644 index 00000000..658a53f8 --- /dev/null +++ b/crates/rupg-types/src/range.rs @@ -0,0 +1,297 @@ +//! The text and binary forms of the range types, from `range_parse`, `range_deparse`, `range_recv` and `range_send` of `rangetypes.c`. A range keeps its bounds as values of the subtype, so these functions give and take the text or the bytes of each bound. The caller reads and writes the bounds with the functions of the subtype. + +use rupg_common::SqlState; + +use crate::binary::Recv; +use crate::error::TypeError; +use crate::value::Value; + +/// `RANGE_EMPTY`: the range has no points. +pub const RANGE_EMPTY: u8 = 0x01; +/// `RANGE_LB_INC`: the lower bound is part of the range. +pub const RANGE_LB_INC: u8 = 0x02; +/// `RANGE_UB_INC`: the upper bound is part of the range. +pub const RANGE_UB_INC: u8 = 0x04; +/// `RANGE_LB_INF`: the range has no lower bound. +pub const RANGE_LB_INF: u8 = 0x08; +/// `RANGE_UB_INF`: the range has no upper bound. +pub const RANGE_UB_INF: u8 = 0x10; + +/// The text of an empty range. +const EMPTY_LITERAL: &str = "empty"; + +/// A value of a range type: the flags of `rangetypes.h` and the two bounds. A bound that is infinite is `Value::Null`, and so are both bounds of an empty range. +#[derive(Clone, Debug, PartialEq)] +pub struct Range { + pub flags: u8, + pub lower: Value, + pub upper: Value, +} + +impl Range { + /// The empty range. + pub fn empty() -> Range { + Range { flags: RANGE_EMPTY, lower: Value::Null, upper: Value::Null } + } + + /// True for an empty range. + pub fn is_empty(&self) -> bool { + self.flags & RANGE_EMPTY != 0 + } + + /// `RANGE_HAS_LBOUND`: the range has a lower bound value. + pub fn has_lower(&self) -> bool { + self.flags & (RANGE_EMPTY | RANGE_LB_INF) == 0 + } + + /// `RANGE_HAS_UBOUND`: the range has an upper bound value. + pub fn has_upper(&self) -> bool { + self.flags & (RANGE_EMPTY | RANGE_UB_INF) == 0 + } +} + +/// A range as `range_parse` reads it: the flags, and the text of each bound that is not infinite. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RangeText { + pub flags: u8, + pub lower: Option, + pub upper: Option, +} + +/// `isspace` of C in the C locale. +fn is_space(b: u8) -> bool { + matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C) +} + +/// The error of a range literal that does not parse. +fn malformed(text: &str, detail: &str) -> TypeError { + let mut error = TypeError::new( + SqlState::INVALID_TEXT_REPRESENTATION, + format!("malformed range literal: \"{text}\""), + ); + error.detail = Some(detail.to_string()); + error +} + +/// `range_parse`: the key word `empty`, or a bracket or a parenthesis, the lower bound, a comma, the upper bound and a bracket or a parenthesis. White space before and after the range is not part of it. A bound with no characters is infinite. A bound can have characters in double quotes, and a backslash takes the next character as it is. +/// +/// # Errors +/// +/// `22P02` "malformed range literal" with the detail of PostgreSQL. +pub fn range_parse(text: &str) -> Result { + let s = text.as_bytes(); + let mut at = 0; + while at < s.len() && is_space(s[at]) { + at += 1; + } + if s.len() - at >= EMPTY_LITERAL.len() + && s[at..at + EMPTY_LITERAL.len()].eq_ignore_ascii_case(EMPTY_LITERAL.as_bytes()) + { + at += EMPTY_LITERAL.len(); + while at < s.len() && is_space(s[at]) { + at += 1; + } + if at < s.len() { + return Err(malformed(text, "Junk after \"empty\" key word.")); + } + return Ok(RangeText { flags: RANGE_EMPTY, lower: None, upper: None }); + } + let mut flags = 0; + match s.get(at) { + Some(b'[') => flags |= RANGE_LB_INC, + Some(b'(') => {} + _ => return Err(malformed(text, "Missing left parenthesis or bracket.")), + } + at += 1; + let lower = parse_bound(text, &mut at)?; + if lower.is_none() { + flags |= RANGE_LB_INF; + } + if s.get(at) != Some(&b',') { + return Err(malformed(text, "Missing comma after lower bound.")); + } + at += 1; + let upper = parse_bound(text, &mut at)?; + if upper.is_none() { + flags |= RANGE_UB_INF; + } + match s.get(at) { + Some(b']') => flags |= RANGE_UB_INC, + Some(b')') => {} + _ => return Err(malformed(text, "Too many commas.")), + } + at += 1; + while at < s.len() && is_space(s[at]) { + at += 1; + } + if at < s.len() { + return Err(malformed(text, "Junk after right parenthesis or bracket.")); + } + Ok(RangeText { flags, lower, upper }) +} + +/// `range_parse_bound`: the text of one bound up to a comma, a parenthesis or a bracket that is not in quotes, or `None` for a bound with no characters. In quotes, two double quotes are one double quote. +fn parse_bound(text: &str, at: &mut usize) -> Result, TypeError> { + let s = text.as_bytes(); + let ends = |b: Option<&u8>| matches!(b, Some(b',' | b')' | b']')); + if ends(s.get(*at)) { + return Ok(None); + } + let mut out = Vec::new(); + let mut quoted = false; + while quoted || !ends(s.get(*at)) { + let Some(&c) = s.get(*at) else { + return Err(malformed(text, "Unexpected end of input.")); + }; + *at += 1; + match c { + b'\\' => { + let Some(&next) = s.get(*at) else { + return Err(malformed(text, "Unexpected end of input.")); + }; + out.push(next); + *at += 1; + } + b'"' if !quoted => quoted = true, + b'"' if s.get(*at) == Some(&b'"') => { + out.push(b'"'); + *at += 1; + } + b'"' => quoted = false, + _ => out.push(c), + } + } + Ok(Some(String::from_utf8_lossy(&out).into_owned())) +} + +/// `range_deparse`: the text of a range from the flags and the output of each bound that the range has. +pub fn range_deparse(flags: u8, lower: Option<&[u8]>, upper: Option<&[u8]>, out: &mut Vec) { + if flags & RANGE_EMPTY != 0 { + out.extend_from_slice(EMPTY_LITERAL.as_bytes()); + return; + } + out.push(if flags & RANGE_LB_INC != 0 { b'[' } else { b'(' }); + if let Some(lower) = lower { + bound_escape(lower, out); + } + out.push(b','); + if let Some(upper) = upper { + bound_escape(upper, out); + } + out.push(if flags & RANGE_UB_INC != 0 { b']' } else { b')' }); +} + +/// `range_bound_escape`: a bound that is empty or has a quote, a backslash, a parenthesis, a bracket, a comma or white space is in double quotes, with each quote and each backslash doubled. +fn bound_escape(value: &[u8], out: &mut Vec) { + let quote = value.is_empty() + || value + .iter() + .any(|&b| matches!(b, b'"' | b'\\' | b'(' | b')' | b'[' | b']' | b',') || is_space(b)); + if !quote { + out.extend_from_slice(value); + return; + } + out.push(b'"'); + for &b in value { + if matches!(b, b'"' | b'\\') { + out.push(b); + } + out.push(b); + } + out.push(b'"'); +} + +/// The flags and the bytes of each bound of a range in the binary form. +pub type RangeBytes<'a> = (u8, Option<&'a [u8]>, Option<&'a [u8]>); + +/// `range_recv`: the flags, then the length and the bytes of each bound that the flags give. The flags lose the bits that are not flags of a range. +/// +/// # Errors +/// +/// `08P01` for a value that is too short or too long. +pub fn range_recv<'a>(recv: &mut Recv<'a>) -> Result, TypeError> { + let flags = + recv.byte()? & (RANGE_EMPTY | RANGE_LB_INC | RANGE_LB_INF | RANGE_UB_INC | RANGE_UB_INF); + let mut bound = |inf: u8| -> Result, TypeError> { + if flags & (RANGE_EMPTY | inf) != 0 { + return Ok(None); + } + let len = recv.u32()?; + recv.bytes(usize::try_from(len).unwrap_or(usize::MAX)).map(Some) + }; + let lower = bound(RANGE_LB_INF)?; + let upper = bound(RANGE_UB_INF)?; + if recv.remaining() != 0 { + return Err(TypeError::new( + SqlState::PROTOCOL_VIOLATION, + "invalid message format".to_string(), + )); + } + Ok((flags, lower, upper)) +} + +/// `range_send`: the flags, then the length and the bytes of each bound that the range has. +pub fn range_send(flags: u8, lower: Option<&[u8]>, upper: Option<&[u8]>, out: &mut Vec) { + out.push(flags); + for bound in [lower, upper].into_iter().flatten() { + let len = u32::try_from(bound.len()).unwrap_or(u32::MAX); + out.extend_from_slice(&len.to_be_bytes()); + out.extend_from_slice(bound); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(text: &str) -> (u8, Option<&'static str>, Option<&'static str>) { + let r = range_parse(text).unwrap(); + let leak = |s: Option| s.map(|s| &*Box::leak(s.into_boxed_str())); + (r.flags, leak(r.lower), leak(r.upper)) + } + + fn detail(text: &str) -> String { + range_parse(text).unwrap_err().detail.unwrap() + } + + #[test] + fn text_forms() { + assert_eq!(parse("[1,10)"), (RANGE_LB_INC, Some("1"), Some("10"))); + assert_eq!(parse(" (,] "), (RANGE_LB_INF | RANGE_UB_INF | RANGE_UB_INC, None, None)); + assert_eq!(parse(" EMPTY "), (RANGE_EMPTY, None, None)); + assert_eq!(parse(r#"["a ""b"" \c",x\,y]"#).1, Some(r#"a "b" c"#)); + assert_eq!(parse(r#"["a ""b"" \c",x\,y]"#).2, Some("x,y")); + assert_eq!(parse(r#"["",)"#).1, Some("")); + assert_eq!(detail("emptyx"), "Junk after \"empty\" key word."); + assert_eq!(detail(""), "Missing left parenthesis or bracket."); + assert_eq!(detail("[1 10)"), "Missing comma after lower bound."); + assert_eq!(detail("[1,10"), "Unexpected end of input."); + assert_eq!(detail("[1,10,11)"), "Too many commas."); + assert_eq!(detail("[1,10)x"), "Junk after right parenthesis or bracket."); + assert_eq!(detail("[1)"), "Missing comma after lower bound."); + assert_eq!(detail("[1,2\\"), "Unexpected end of input."); + let mut out = Vec::new(); + range_deparse(RANGE_LB_INC, Some(b"a b"), Some(b"x\"y"), &mut out); + assert_eq!(out, br#"["a b","x""y")"#); + out.clear(); + range_deparse(RANGE_LB_INF | RANGE_UB_INC, None, Some(b""), &mut out); + assert_eq!(out, br#"(,""]"#); + } + + #[test] + fn binary_forms() { + let mut out = Vec::new(); + range_send(RANGE_LB_INC, Some(&[0, 0, 0, 1]), Some(&[0, 0, 0, 10]), &mut out); + assert_eq!(out, [2, 0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 10]); + let mut recv = Recv::new(&out); + let (flags, lower, upper) = range_recv(&mut recv).unwrap(); + assert_eq!( + (flags, lower, upper), + (RANGE_LB_INC, Some(&[0, 0, 0, 1][..]), Some(&[0, 0, 0, 10][..])) + ); + let mut recv = Recv::new(&[0x81]); + assert_eq!(range_recv(&mut recv).unwrap(), (RANGE_EMPTY, None, None)); + let mut recv = Recv::new(&[RANGE_EMPTY, 0]); + assert_eq!(range_recv(&mut recv).unwrap_err().message, "invalid message format"); + } +} diff --git a/crates/rupg-types/src/value.rs b/crates/rupg-types/src/value.rs index 1f94b620..ef18dc21 100644 --- a/crates/rupg-types/src/value.rs +++ b/crates/rupg-types/src/value.rs @@ -6,6 +6,7 @@ use crate::array::Array; use crate::datetime::Interval; use crate::network::Inet; use crate::numeric::Numeric; +use crate::range::Range; /// One value of the engine. #[derive(Clone, Debug, PartialEq)] @@ -44,6 +45,8 @@ pub enum Value { Array(Box>), /// A row of a composite type or of `record`. Record(Box), + /// A value of a range type. + Range(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.