Skip to content

Conversation

@yutannihilation
Copy link
Contributor

@yutannihilation yutannihilation commented Jan 11, 2026

This pull request implements ST_Affine(), ST_Rotate and ST_Scale using the glam crate. I chose glam because

  • while affine transformation is probably not very hard to implement, I might make some silly mistakes
  • glam is widely used
  • glam doesn't require any additional dependency by default

but I don't have strong opinion. If it is desirable to implement from scratch, I can do so.

> SELECT ST_Affine(ST_GeomFromText('POLYGON ZM ((1 0 1 1, 1 1 1 1, 2 2 2 1, 1 0 1 1))'), 10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0);
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ st_affine(st_geomfromtext(Utf8("POLYGON ZM ((1 0 1 1, 1 1 1 1, 2 2 2 1, 1 0 1 1))")),Int64(10),Int64(0),Int64(0),Int64(0),Int64(10),Int64(0),Int64(0),Int64(0),Int64(10),Int64(0),Int64(0),Int64(0)) │
│                                                                                               geometry                                                                                               │
╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ POLYGON ZM((10 0 10 1,10 10 10 1,20 20 20 1,10 0 10 1))                                                                                                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
  • ST_Affine()
  • ST_Rotate()
    • angle
    • angle and origin (I'll address in another pull request)
  • ST_Scale()
    • scale
    • scale and origin (I'll address in another pull request)

@yutannihilation yutannihilation changed the title feat(rust/sedona-functions): Implement ST_Affine() feat(rust/sedona-functions): Implement ST_Affine(), ST_Rotate(), and ST_Scale() Jan 11, 2026
@yutannihilation yutannihilation marked this pull request as ready for review January 11, 2026 09:17
@yutannihilation
Copy link
Contributor Author

I was hoping to add the variants with origin of rotation or scaling, but, since this pull request is already a bit lengthy, I think it's probably better to address in a separate pull request.

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! This has to be the most arguments I have ever seen in a SQL function and managing all of them is a pain that you've nicely abstracted here.

using the glam crate.

I think this is OK. We sort of have this for 2D already but I'm not sure that will simplify this PR given the 3D support. If we do inline this we should do it in sedona-geometry and add the appropriate tests.

I was hoping to add the variants with origin of rotation or scaling, but, since this pull request is already a bit lengthy, I think it's probably better to address in a separate pull request.

Great!

let executor = WkbExecutor::new(arg_types, args);
let mut builder = BinaryBuilder::with_capacity(
executor.num_iterations(),
WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do this here because I think we need a helper for it to be compact, but in this particular case we know the output will have the exact number of bytes as the input)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, true. Thanks for catching.

Comment on lines 46 to 57
match geom.as_type() {
geo_traits::GeometryType::Point(pt) => {
if pt.coord().is_some() {
write_wkb_point_header(writer, dims)
.map_err(|e| DataFusionError::Execution(e.to_string()))?;
write_transformed_coord(writer, pt.coord().unwrap(), mat, dim)?;
} else {
write_wkb_empty_point(writer, dims)
.map_err(|e| DataFusionError::Execution(e.to_string()))?;
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this can be written as a CrsTransform we might be able re-use some existing infrastructure:

#[derive(Debug)]
struct Translate {
deltax: f64,
deltay: f64,
}
impl CrsTransform for Translate {
fn transform_coord(&self, coord: &mut (f64, f64)) -> Result<(), SedonaGeometryError> {
coord.0 += self.deltax;
coord.1 += self.deltay;
Ok(())
}
}

let trans = Translate { deltax, deltay };
transform(wkb, &trans, &mut builder)
.map_err(|e| DataFusionError::External(Box::new(e)))?;

https://github.com/apache/sedona-db/blob/main/rust/sedona-geometry/src/transform.rs#L259-L263

There's a chance what you have here is faster, though (no dyn like our current transform()), and we haven't implemented non-XY support for the CrsTransform yet ( #47 ).

No need to take on any of that here (I'll just add a note to that issue that we can update this section when we do get there).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. I'll try.

(As translation can also be handled by affine transformation, I was wondering if ST_Transform can also be implemented using glam. But, I guess there will be not much difference)

Comment on lines 130 to 150
fn write_transformed_coords<I>(
writer: &mut impl Write,
coords: I,
affine: &DAffine,
dim: &geo_traits::Dimensions,
) -> Result<()>
where
I: DoubleEndedIterator,
I::Item: CoordTrait<T = f64>,
{
coords
.into_iter()
.try_for_each(|coord| write_transformed_coord(writer, coord, affine, dim))
}

fn write_transformed_coord<C>(
writer: &mut impl Write,
coord: C,
affine: &DAffine,
dim: &geo_traits::Dimensions,
) -> Result<()>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Optional) It may be unfounded, but my intuition would be that match (dim) should be in write_transformed_coords() to avoid a match() for every coordinate. Perhaps the compiler does this anyway or the transform operation is sufficiently expensive that it doesn't matter 🙂

Comment on lines 185 to 193
pub(crate) struct DAffine2Iterator<'a> {
index: usize,
a: &'a PrimitiveArray<Float64Type>,
b: &'a PrimitiveArray<Float64Type>,
d: &'a PrimitiveArray<Float64Type>,
e: &'a PrimitiveArray<Float64Type>,
x_offset: &'a PrimitiveArray<Float64Type>,
y_offset: &'a PrimitiveArray<Float64Type>,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need multiple versions of this (e.g., can the core logic be distilled to struct NumericArgs { args: Vec<&'a PrimitiveArray<Float64Type>> }? Or maybe the lifetimes make this difficult?).

My reading of these is that they don't handle nulls in any of the arguments. I am not sure if it's optimizing this but you could check ahead of time if the null_count() is >0 for any of the inputs and use the slower iterator if this is the case.

Co-authored-by: Dewey Dunnington <[email protected]>
Copy link
Contributor Author

@yutannihilation yutannihilation left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing (and sorry for a bit massive PR)! I'll try using CrsTransform.

let executor = WkbExecutor::new(arg_types, args);
let mut builder = BinaryBuilder::with_capacity(
executor.num_iterations(),
WKB_MIN_PROBABLE_BYTES * executor.num_iterations(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, true. Thanks for catching.

Comment on lines 46 to 57
match geom.as_type() {
geo_traits::GeometryType::Point(pt) => {
if pt.coord().is_some() {
write_wkb_point_header(writer, dims)
.map_err(|e| DataFusionError::Execution(e.to_string()))?;
write_transformed_coord(writer, pt.coord().unwrap(), mat, dim)?;
} else {
write_wkb_empty_point(writer, dims)
.map_err(|e| DataFusionError::Execution(e.to_string()))?;
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me. I'll try.

(As translation can also be handled by affine transformation, I was wondering if ST_Transform can also be implemented using glam. But, I guess there will be not much difference)

@yutannihilation yutannihilation marked this pull request as draft January 12, 2026 23:16
@yutannihilation yutannihilation marked this pull request as ready for review January 13, 2026 14:34
@yutannihilation
Copy link
Contributor Author

I think I addressed most of the comments. Thanks for the advices!

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to get through all of this before the end of the day but I may have missed some things. Thank you for these edits...I think it is very close!

Copy link
Member

@paleolimbot paleolimbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@paleolimbot paleolimbot merged commit 3f4acd0 into apache:main Jan 14, 2026
15 checks passed
@yutannihilation yutannihilation deleted the feat/st_affine branch January 14, 2026 02:27
@yutannihilation
Copy link
Contributor Author

Thanks so much for reviewing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants