8000 feat(postgres): read bounds, a divisor and a pattern from a check by colinhacks · Pull Request #200 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions engine/core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,8 @@ impl Type {
/// The type in one short line, as the oracle's `expected.txt` and the engines' tests print it:
/// its name (else its kind), then what narrows it in parentheses: an enum's values
/// (`varchar('a' | 'b')`), a size (`varchar(≤255)`, `bytes(=6)`), a decimal's precision and
/// scale (`numeric(10,2)`), an integer's bounds (`int16(0..2155)`). An array is its element and
/// scale (`numeric(10,2)`), a number's bounds (`int16(0..2155)`, `numeric(>0..<1)`) and divisor
/// (`int4(×5)`). An array is its element and
/// `[]`, `?` after an element that may be null (`text?[]`); a record is its fields
/// (`point{ x: float8, y: float8 }`, `?` after a field that may be null and before one that
/// may be absent); JSON a statement built is its content after its name (`json { id: int4 }`),
Expand All @@ -943,20 +944,25 @@ impl Type {
Kind::Integer(i) => {
let label = self.name.clone().unwrap_or_else(|| format!("{}int{}", if i.unsigned { "u" } else { "" }, i.bits));
let mut facts = Vec::new();
if !i.bounds.is_empty() {
facts.push(format!("{}..{}", i.bounds.min.as_deref().unwrap_or(""), i.bounds.max.as_deref().unwrap_or("")));
}
facts.extend(short_bounds(&i.bounds));
facts.extend(i.multiple_of.as_ref().map(|m| format!("×{m}")));
if let Some(values) = &i.values {
facts.push(values.join(" | "));
}
if facts.is_empty() { label } else { format!("{label}({})", facts.join(", ")) }
}
Kind::Float(f) if self.name.is_none() => format!("float({},{})", f.exponent_bits, f.mantissa_bits),
Kind::Decimal(d) => match (d.precision, d.min_scale, d.max_scale) {
(Some(p), Some(min), Some(max)) if min == max => with(vec![format!("{p},{min}")]),
(Some(p), _, _) => with(vec![p.to_string()]),
_ => label(),
},
Kind::Float(f) => with(short_bounds(&f.bounds).into_iter().collect()),
Kind::Decimal(d) => {
let mut facts = match (d.precision, d.min_scale, d.max_scale) {
(Some(p), Some(min), Some(max)) if min == max => vec![format!("{p},{min}")],
(Some(p), _, _) => vec![p.to_string()],
_ => Vec::new(),
};
facts.extend(short_bounds(&d.bounds));
facts.extend(d.multiple_of.as_ref().map(|m| format!("×{m}")));
with(facts)
}
Kind::Text(t) => {
let mut facts = size(&t.size);
if let Some(values) = &t.values {
Expand Down Expand Up @@ -1169,6 +1175,18 @@ fn check_order(b: &Bounds, order: fn(&str, &str) -> Option<std::cmp::Ordering>)
Ok(())
}

/// A number's bounds in the short notation (`0..150`, `>0..<1`), but for those an exponent writes:
/// the reach of the type itself (`numeric`'s `1e131072`), which says nothing of the column.
fn short_bounds(b: &Bounds) -> Option<String> {
let side = |inclusive: &Option<String>, exclusive: &Option<String>, mark: &str| match (inclusive, exclusive) {
(Some(v), _) if !v.contains('e') => v.clone(),
(_, Some(v)) if !v.contains('e') => format!("{mark}{v}"),
_ => String::new(),
};
let (low, high) = (side(&b.min, &b.exclusive_min, ">"), side(&b.max, &b.exclusive_max, "<"));
(!low.is_empty() || !high.is_empty()).then(|| format!("{low}..{high}"))
}

/// Two numbers in canonical decimal text, compared, where both read as `f64` without loss of
/// order (an exponent such as `1e131072` reads as infinity, which still orders it).
fn decimal_order(a: &str, b: &str) -> Option<std::cmp::Ordering> {
Expand Down
6 changes: 3 additions & 3 deletions engine/postgres/src/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3764,7 +3764,7 @@ mod tests {
}

fn col(nullable: bool, type_oid: u32) -> DescribedColumn {
DescribedColumn { name: "a".into(), type_oid, typmod: -1, type_name: String::new(), nullable, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), whole_row: false }
DescribedColumn { name: "a".into(), type_oid, typmod: -1, type_name: String::new(), nullable, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), facts: Default::default(), whole_row: false }
}

#[test]
Expand Down Expand Up @@ -4640,8 +4640,8 @@ mod tests {
assert_eq!(p.head[1], None);
assert_eq!(p.leaves.as_deref(), Some("SELECT u AS a0 FROM users u WHERE u.id = NULL"));
// The probe says whether it was the row after all: a column named like the alias is not.
let row = DescribedColumn { name: "whole".into(), type_oid: 16500, typmod: -1, type_name: "users".into(), nullable: true, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), whole_row: false };
let column = DescribedColumn { name: "whole".into(), type_oid: oid::TEXT, typmod: -1, type_name: "text".into(), nullable: true, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), whole_row: false };
let row = DescribedColumn { name: "whole".into(), type_oid: 16500, typmod: -1, type_name: "users".into(), nullable: true, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), facts: Default::default(), whole_row: false };
let column = DescribedColumn { name: "whole".into(), type_oid: oid::TEXT, typmod: -1, type_name: "text".into(), nullable: true, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), facts: Default::default(), whole_row: false };
assert!(resolve(p.head[0].as_ref().unwrap(), Some(&[row])).non_null);
assert!(!resolve(p.head[0].as_ref().unwrap(), Some(&[column])).non_null);
assert_eq!(pg("SELECT p FROM users u LEFT JOIN posts p ON p.user_id = u.id").head[0], None);
Expand Down
4 changes: 2 additions & 2 deletions engine/postgres/src/jsonshape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,7 @@ impl ResolveCtx<'_> {
let ty = if col.whole_row { self.registry.render_row(col.type_oid) } else { crate::postgres::nest(col, self.registry) };
// A built array of arrays carries its elements' nullability at its innermost level, as a result column does.
let items = if col.nested.is_some() { None } else { col.nullable_items };
col.non_finite.apply(crate::postgres::with_document(crate::postgres::with_values(crate::postgres::with_items(ty, items), col.values.as_deref(), self.registry.holds_strings(col.type_oid)), col.document.as_ref()))
col.facts.apply(col.non_finite.apply(crate::postgres::with_document(crate::postgres::with_values(crate::postgres::with_items(ty, items), col.values.as_deref(), self.registry.holds_strings(col.type_oid)), col.document.as_ref())))
}

/// One shape as (type, whether the value can never be null).
Expand Down Expand Up @@ -2392,7 +2392,7 @@ mod tests {
const BYTEA: u32 = 17;

fn column(name: &str, type_oid: u32, type_name: &str, nullable: bool) -> DescribedColumn {
DescribedColumn { name: name.into(), type_oid, typmod: -1, type_name: type_name.into(), nullable, nullable_items: None, nested: None, values: None, document: None, non_finite: Default::default(), whole_row: false }
DescribedColumn { name: name.into(), type_oid, typmod: -1, type_name: type_name.into(), nullable, nullable_items: None, nested: None, values: None, document: None, non_finite: Defau 8BD0 lt::default(), facts: Default::default(), whole_row: false }
}

#[test]
Expand Down
19 changes: 14 additions & 5 deletions engine/postgres/src/pgtypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ pub struct TypeRegistry {
/// By relation and attribute, the non-finite values a CHECK keeps out of a float or `numeric`
/// column of a composite's relation, which a stored row of it holds no more.
pub non_finite: HashMap<(u32, i32), crate::types::NonFinite>,
/// What a CHECK holds each column of a stored row to ([`crate::types::CheckFacts`]), by relation
/// and attribute.
pub facts: HashMap<(u32, i32), crate::types::CheckFacts>,
}

/// Whether every row a scan of relation `$rel` reads has a value in the column named `$col`, as
Expand Down Expand Up @@ -486,9 +489,10 @@ impl TypeRegistry {
}

/// The float and `numeric` columns of the relation whose row type `oid` is, by relation and
/// attribute: the ones a CHECK may keep NaN or the infinities out of ([`TypeRegistry::non_finite`]).
/// attribute: the ones a CHECK may hold to bounds or a divisor ([`TypeRegistry::facts`]), or keep
/// NaN or the infinities out of ([`TypeRegistry::non_finite`]).
pub fn row_number_columns(&self, oid: u32) -> Vec<(u32, i32)> {
self.row_columns(oid, &[oid::FLOAT4, oid::FLOAT8, oid::NUMERIC])
self.row_columns(oid, &[oid::INT2, oid::INT4, oid::INT8, oid::FLOAT4, oid::FLOAT8, oid::NUMERIC])
}

fn row_columns(&self, oid: u32, types: &[u32]) -> Vec<(u32, i32)> {
Expand Down Expand Up @@ -544,10 +548,11 @@ impl TypeRegistry {
matches!(base, oid::TEXT | oid::VARCHAR) || self.get(base).filter(|t| t.kind == TypeKind::Array).and_then(|t| t.elem).is_some_and(|e| self.is_string(e))
}

/// Whether type `oid` is a float or `numeric`, through any domain: a number a CHECK may keep
/// NaN or the infinities out of ([`crate::types::NonFinite`]).
/// Whether type `oid` is a number, through any domain: one a CHECK may hold to bounds or a
/// divisor ([`crate::types::CheckFacts`]), or keep NaN or the infinities out of
/// ([`crate::types::NonFinite`]).
pub fn is_number(&self, oid: u32) -> bool {
matches!(self.resolve_domain(oid), oid::FLOAT4 | oid::FLOAT8 | oid::NUMERIC)
matches!(self.resolve_domain(oid), oid::INT2 | oid::INT4 | oid::INT8 | oid::FLOAT4 | oid::FLOAT8 | oid::NUMERIC)
}

/// Each domain among `names` with the name of the base type it is made from, through any
Expand Down Expand Up @@ -602,6 +607,10 @@ impl TypeRegistry {
(_, _, Some(kept_out)) => kept_out.apply(ty),
_ => ty,
};
let ty = match t.relid.filter(|_| stored).and_then(|r| self.facts.get(&(r, f.attnum))) {
Some(facts) => facts.apply(ty),
None => ty,
};
Field::new(f.name.clone(), ty, !(stored && f.not_null))
})
.collect();
Expand Down
Loading
Loading
0