use wbvector::{Feature, FieldType, FieldValue, GeometryType, Layer};
#[derive(Debug, Clone, PartialEq)]
pub struct FieldProfile {
pub non_null: usize,
pub all_int32: bool,
pub no_int_beyond_int32: bool,
pub longest_int: usize,
pub longest_text: usize,
pub all_dates: bool,
pub all_datetimes: bool,
pub with_offset: usize,
}
impl Default for FieldProfile {
fn default() -> Self {
Self {
non_null: 0,
all_int32: true,
no_int_beyond_int32: true,
longest_int: 0,
longest_text: 0,
all_dates: true,
all_datetimes: true,
with_offset: 0,
}
}
}
pub(crate) fn text_len(v: &FieldValue) -> usize {
match v {
FieldValue::Text(s) | FieldValue::Date(s) | FieldValue::DateTime(s) => s.len(),
FieldValue::Integer(x) => x.to_string().len(),
FieldValue::Float(x) => x.to_string().len(),
FieldValue::Boolean(b) => b.to_string().len(),
FieldValue::Blob(b) => 2 * b.len(),
FieldValue::Null => 0,
}
}
fn int_len(x: i64) -> usize {
let digits = if x == 0 { 1 } else { x.unsigned_abs().ilog10() as usize + 1 };
digits + usize::from(x < 0)
}
impl FieldProfile {
fn merge(&mut self, o: &FieldProfile) {
self.non_null += o.non_null;
self.all_int32 &= o.all_int32;
self.no_int_beyond_int32 &= o.no_int_beyond_int32;
self.longest_int = self.longest_int.max(o.longest_int);
self.longest_text = self.longest_text.max(o.longest_text);
self.all_dates &= o.all_dates;
self.all_datetimes &= o.all_datetimes;
self.with_offset += o.with_offset;
}
fn add(&mut self, v: &FieldValue, t: FieldType) {
if let Some(x) = v.as_i64() {
self.no_int_beyond_int32 &= i32::try_from(x).is_ok();
self.longest_int = self.longest_int.max(int_len(x));
}
if v.is_null() {
return;
}
self.non_null += 1;
self.all_int32 &= v.as_i64().is_some_and(|x| i32::try_from(x).is_ok());
if !matches!(t, FieldType::Integer | FieldType::Float | FieldType::Boolean | FieldType::Date) {
self.longest_text = self.longest_text.max(text_len(v));
}
if matches!(t, FieldType::Date | FieldType::DateTime) {
let s = v.as_str();
self.all_dates &= s.and_then(super::geoparquet::parse_date).is_some();
self.all_datetimes &= s.and_then(super::geoparquet::parse_datetime).is_some();
if s.is_some_and(crate::arrow::has_offset) {
self.with_offset += 1;
}
}
}
}
fn shared_type(a: GeometryType, b: GeometryType) -> Option<GeometryType> {
use GeometryType as T;
match (a, b) {
(a, b) if a == b => Some(a),
(T::Point | T::MultiPoint, T::Point | T::MultiPoint) => Some(T::MultiPoint),
(T::LineString | T::MultiLineString, T::LineString | T::MultiLineString) => Some(T::MultiLineString),
(T::Polygon | T::MultiPolygon, T::Polygon | T::MultiPolygon) => Some(T::MultiPolygon),
_ => None,
}
}
pub(crate) fn add_type_name(names: &mut Vec<String>, t: GeometryType, z: bool) {
let (base, suffix) = (t.as_str(), if z { " Z" } else { "" });
if !names.iter().any(|n| n.strip_prefix(base) == Some(suffix)) {
names.push(format!("{base}{suffix}"));
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GeometryProfile {
pub count: usize,
pub first: Option<(GeometryType, bool)>,
pub shared: Option<Option<GeometryType>>,
pub has_z: bool,
pub any_z: bool,
pub has_m: bool,
pub non_point: bool,
pub missing_bbox: bool,
pub type_names: Vec<String>,
pub bbox: Option<[f64; 4]>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct LayerProfile {
pub count: usize,
pub fields: Vec<FieldProfile>,
pub geometry: GeometryProfile,
types: Vec<FieldType>,
}
impl LayerProfile {
pub fn new(layer: &Layer) -> Self {
let types: Vec<FieldType> = layer.schema.fields().iter().map(|f| f.field_type).collect();
Self { count: 0, fields: vec![FieldProfile::default(); types.len()], geometry: GeometryProfile::default(), types }
}
pub fn of(layer: &Layer) -> Self {
let mut p = Self::new(layer);
p.add(&layer.features);
p
}
pub fn add(&mut self, features: &[Feature]) {
const PART: usize = 4096;
if features.len() <= PART {
return self.add_seq(features);
}
let parts = crate::par::map_range(features.len().div_ceil(PART), |i| {
let mut p = Self {
count: 0,
fields: vec![FieldProfile::default(); self.types.len()],
geometry: GeometryProfile::default(),
types: self.types.clone(),
};
p.add_seq(&features[i * PART..((i + 1) * PART).min(features.len())]);
p
});
for p in &parts {
self.merge(p);
}
}
fn merge(&mut self, o: &LayerProfile) {
self.count += o.count;
for (a, b) in self.fields.iter_mut().zip(&o.fields) {
a.merge(b);
}
let (g, h) = (&mut self.geometry, &o.geometry);
g.count += h.count;
g.first = match (g.first, h.first) {
(None, f) => f,
(Some(f), None) => Some(f),
(Some((t, same)), Some((u, same2))) => Some((t, same && same2 && t == u)),
};
g.shared = match (g.shared, h.shared) {
(None, s) => s,
(s, None) => s,
(Some(Some(a)), Some(Some(b))) => Some(shared_type(a, b)),
_ => Some(None),
};
g.has_z |= h.has_z;
g.any_z |= h.any_z;
g.has_m |= h.has_m;
g.non_point |= h.non_point;
g.missing_bbox |= h.missing_bbox;
for n in &h.type_names {
if !g.type_names.contains(n) {
g.type_names.push(n.clone());
}
}
g.bbox = match (g.bbox, h.bbox) {
(Some(a), Some(b)) => Some([a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]),
(a, b) => a.or(b),
};
}
fn add_seq(&mut self, features: &[Feature]) {
for f in features {
self.count += 1;
for ((fp, v), t) in self.fields.iter_mut().zip(&f.attributes).zip(&self.types) {
fp.add(v, *t);
}
let g = &mut self.geometry;
let Some(geom) = &f.geometry else {
g.missing_bbox = true;
continue;
};
g.count += 1;
let t = geom.geom_type();
g.first = Some(match g.first {
None => (t, true),
Some((first, same)) => (first, same && first == t),
});
g.shared = Some(match g.shared {
None => Some(t),
Some(None) => None,
Some(Some(a)) => shared_type(a, t),
});
let z = crate::vector::geometry_has_z(geom);
g.has_z |= z;
if !(g.any_z && g.has_m) {
let (az, am) = crate::arrow::geometry_dims(geom);
g.any_z |= az;
g.has_m |= am;
}
g.non_point |= !matches!(geom, wbvector::Geometry::Point(_));
add_type_name(&mut g.type_names, t, z);
match geom.bbox() {
Some(b) => {
g.bbox = Some(match g.bbox {
None => [b.min_x, b.min_y, b.max_x, b.max_y],
Some(a) => [a[0].min(b.min_x), a[1].min(b.min_y), a[2].max(b.max_x), a[3].max(b.max_y)],
});
}
None => g.missing_bbox = true,
}
}
}
pub fn uniform_type(&self) -> Option<GeometryType> {
self.geometry.first.filter(|(_, same)| *same).map(|(t, _)| t)
}
pub fn features_type(&self) -> Option<GeometryType> {
self.geometry.shared.flatten()
}
pub fn no_geometry(&self) -> bool {
self.geometry.count == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use wbvector::{FieldDef, FieldType, Geometry};
#[test]
fn profiles_match_whole_layer_decisions() {
let mut l = Layer::new("t");
l.add_field(FieldDef::new("i", FieldType::Integer));
l.add_field(FieldDef::new("d", FieldType::Date));
for (i, (v, g)) in [
(FieldValue::Integer(3_000_000_000), Some(Geometry::point(1.0, 2.0))),
(FieldValue::Integer(-5), Some(Geometry::multi_point(vec![wbvector::Coord::xy(0.0, 0.0)]))),
(FieldValue::Null, None),
]
.into_iter()
.enumerate()
{
l.push(Feature { fid: i as u64, geometry: g, attributes: vec![v, FieldValue::Date("2024-01-02".into())] });
}
let p = LayerProfile::of(&l);
assert_eq!(p.count, 3);
assert_eq!((int_len(0), int_len(-5), int_len(1000), int_len(i64::MIN)), (1, 2, 4, 20));
assert!(!p.fields[0].all_int32 && !p.fields[0].no_int_beyond_int32);
assert_eq!(p.fields[0].longest_int, 10);
assert!(p.fields[1].all_dates);
assert_eq!(p.features_type(), Some(GeometryType::MultiPoint));
assert_eq!(p.uniform_type(), super::super::uniform_type(&l));
assert_eq!(p.geometry.type_names, ["Point", "MultiPoint"]);
assert!(p.geometry.missing_bbox);
let mut q = LayerProfile::new(&l);
q.add(&l.features[..1]);
q.add(&l.features[1..]);
assert_eq!(p, q);
}
#[test]
fn parallel_profiles_match_sequential_ones() {
let mut l = Layer::new("t");
l.add_field(FieldDef::new("i", FieldType::Integer));
l.add_field(FieldDef::new("d", FieldType::Date));
l.add_field(FieldDef::new("s", FieldType::Text));
for k in 0..20_000i64 {
let geometry = match k % 7 {
0 => None,
1 | 2 => Some(Geometry::point(k as f64, -(k as f64))),
3 => Some(Geometry::multi_point(vec![wbvector::Coord::xyz(1.0, 2.0, k as f64)])),
_ if k > 15_000 => Some(Geometry::line_string(vec![wbvector::Coord::xy(0.0, 0.0), wbvector::Coord::xy(k as f64, 1.0)])),
_ => Some(Geometry::point(0.5, 0.5)),
};
let i = if k == 12_345 { FieldValue::Integer(1 << 40) } else { FieldValue::Integer(k) };
let d = if k == 19_000 { FieldValue::Date("not a date".into()) } else { FieldValue::Date("2024-01-02".into()) };
let s = if k % 11 == 0 { FieldValue::Null } else { FieldValue::Text("x".repeat((k % 300) as usize)) };
l.push(Feature { fid: k as u64, geometry, attributes: vec![i, d, s] });
}
let parallel = LayerProfile::of(&l);
let mut sequential = LayerProfile::new(&l);
sequential.add_seq(&l.features);
assert_eq!(parallel, sequential);
let mut p = LayerProfile::new(&l);
p.add(&l.features[..1]);
p.add(&l.features[1..]);
assert_eq!(p, sequential);
}
}