This is an automated email from the ASF dual-hosted git repository.

kontinuation pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new 2822b306 feat(sedona-gdal): add dataset and vector/raster wrappers 
(#699)
2822b306 is described below

commit 2822b30680a7b46ddd12fd1f32eacb576a16abac
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Mar 26 21:57:00 2026 +0800

    feat(sedona-gdal): add dataset and vector/raster wrappers (#699)
    
    ## Summary
    - add safe wrappers for GDAL datasets, drivers, raster bands, features, and 
layers
    - wire the core raster/vector modules that higher-level operations build on
    - keep the wrapper surface explicit by removing module re-export aliases 
from this layer
---
 c/sedona-gdal/src/dataset.rs           | 501 +++++++++++++++++++++++++++++++++
 c/sedona-gdal/src/driver.rs            | 242 ++++++++++++++++
 c/sedona-gdal/src/errors.rs            |   3 +
 c/sedona-gdal/src/lib.rs               |   2 +
 c/sedona-gdal/src/raster.rs            |   1 +
 c/sedona-gdal/src/raster/rasterband.rs | 372 ++++++++++++++++++++++++
 c/sedona-gdal/src/vector.rs            |   2 +
 c/sedona-gdal/src/vector/feature.rs    | 213 ++++++++++++++
 c/sedona-gdal/src/vector/layer.rs      | 208 ++++++++++++++
 c/sedona-gdal/src/vsi.rs               |   2 +-
 10 files changed, 1545 insertions(+), 1 deletion(-)

diff --git a/c/sedona-gdal/src/dataset.rs b/c/sedona-gdal/src/dataset.rs
new file mode 100644
index 00000000..36832d86
--- /dev/null
+++ b/c/sedona-gdal/src/dataset.rs
@@ -0,0 +1,501 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/dataset.rs>.
+//! Original code is licensed under MIT.
+
+use std::ffi::{CStr, CString};
+use std::ptr;
+
+use crate::cpl::CslStringList;
+use crate::driver::Driver;
+use crate::errors::Result;
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::raster::rasterband::RasterBand;
+use crate::raster::types::{DatasetOptions, GdalDataType as RustGdalDataType};
+use crate::spatial_ref::SpatialRef;
+use crate::vector::layer::Layer;
+
+/// A GDAL dataset.
+pub struct Dataset {
+    api: &'static GdalApi,
+    c_dataset: GDALDatasetH,
+}
+
+// SAFETY: `Dataset` has unique ownership of its GDAL dataset handle and only 
moves
+// that ownership across threads. The handle is closed exactly once on drop, 
and this
+// wrapper does not provide shared concurrent access, so `Send` is sound while 
`Sync`
+// remains intentionally unimplemented.
+unsafe impl Send for Dataset {}
+
+impl Drop for Dataset {
+    fn drop(&mut self) {
+        if !self.c_dataset.is_null() {
+            unsafe { call_gdal_api!(self.api, GDALClose, self.c_dataset) };
+        }
+    }
+}
+
+impl Dataset {
+    /// Open a dataset with extended options.
+    pub fn open_ex(
+        api: &'static GdalApi,
+        path: &str,
+        open_flags: GDALOpenFlags,
+        allowed_drivers: Option<&[&str]>,
+        open_options: Option<&[&str]>,
+        sibling_files: Option<&[&str]>,
+    ) -> Result<Self> {
+        let c_path = CString::new(path)?;
+
+        // Build CslStringLists from Option<&[&str]>.
+        // None → null pointer (use GDAL default).
+        // Some(&[]) → pointer to [null] (explicitly empty list).
+        let drivers_csl = allowed_drivers
+            .map(|v| CslStringList::try_from_iter(v.iter().copied()))
+            .transpose()?;
+        let options_csl = open_options
+            .map(|v| CslStringList::try_from_iter(v.iter().copied()))
+            .transpose()?;
+        let siblings_csl = sibling_files
+            .map(|v| CslStringList::try_from_iter(v.iter().copied()))
+            .transpose()?;
+
+        let c_dataset = unsafe {
+            call_gdal_api!(
+                api,
+                GDALOpenEx,
+                c_path.as_ptr(),
+                open_flags,
+                drivers_csl
+                    .as_ref()
+                    .map_or(ptr::null(), |csl| csl.as_ptr() as *const *const 
_),
+                options_csl
+                    .as_ref()
+                    .map_or(ptr::null(), |csl| csl.as_ptr() as *const *const 
_),
+                siblings_csl
+                    .as_ref()
+                    .map_or(ptr::null(), |csl| csl.as_ptr() as *const *const _)
+            )
+        };
+
+        if c_dataset.is_null() {
+            return Err(api.last_cpl_err(CE_Failure as u32));
+        }
+
+        Ok(Self { api, c_dataset })
+    }
+
+    /// Create a new Dataset from an owned C handle.
+    pub(crate) fn new(api: &'static GdalApi, c_dataset: GDALDatasetH) -> Self {
+        Self { api, c_dataset }
+    }
+
+    /// Return the raw C dataset handle.
+    pub fn c_dataset(&self) -> GDALDatasetH {
+        self.c_dataset
+    }
+
+    /// Return raster size as (x_size, y_size).
+    pub fn raster_size(&self) -> (usize, usize) {
+        let x = unsafe { call_gdal_api!(self.api, GDALGetRasterXSize, 
self.c_dataset) };
+        let y = unsafe { call_gdal_api!(self.api, GDALGetRasterYSize, 
self.c_dataset) };
+        (x as usize, y as usize)
+    }
+
+    /// Return the number of raster bands.
+    pub fn raster_count(&self) -> usize {
+        unsafe { call_gdal_api!(self.api, GDALGetRasterCount, self.c_dataset) 
as usize }
+    }
+
+    /// Fetch a raster band by 1-indexed band number.
+    /// Band numbers start at 1, as in GDAL.
+    pub fn rasterband(&self, band_index: usize) -> Result<RasterBand<'_>> {
+        let band_index_i32 = i32::try_from(band_index)?;
+        let c_band =
+            unsafe { call_gdal_api!(self.api, GDALGetRasterBand, 
self.c_dataset, band_index_i32) };
+        if c_band.is_null() {
+            return Err(self.api.last_null_pointer_err("GDALGetRasterBand"));
+        }
+        Ok(RasterBand::new(self.api, c_band, self))
+    }
+
+    /// Fetch the dataset geotransform coefficients.
+    /// Return an error if no geotransform is available.
+    pub fn geo_transform(&self) -> Result<[f64; 6]> {
+        let mut gt = [0.0f64; 6];
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALGetGeoTransform,
+                self.c_dataset,
+                gt.as_mut_ptr()
+            )
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(gt)
+    }
+
+    /// Set the geo-transform.
+    pub fn set_geo_transform(&self, gt: &[f64; 6]) -> Result<()> {
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALSetGeoTransform,
+                self.c_dataset,
+                gt.as_ptr() as *mut f64
+            )
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Fetch the projection definition string for this dataset.
+    /// Return an empty string if no projection is available.
+    pub fn projection(&self) -> String {
+        unsafe {
+            let ptr = call_gdal_api!(self.api, GDALGetProjectionRef, 
self.c_dataset);
+            if ptr.is_null() {
+                String::new()
+            } else {
+                CStr::from_ptr(ptr).to_string_lossy().into_owned()
+            }
+        }
+    }
+
+    /// Set the projection definition string for this dataset.
+    pub fn set_projection(&self, projection: &str) -> Result<()> {
+        let c_projection = CString::new(projection)?;
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALSetProjection,
+                self.c_dataset,
+                c_projection.as_ptr()
+            )
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Fetch the spatial reference for this dataset.
+    /// GDAL returns a borrowed handle; this method clones it.
+    pub fn spatial_ref(&self) -> Result<SpatialRef> {
+        let c_srs = unsafe { call_gdal_api!(self.api, GDALGetSpatialRef, 
self.c_dataset) };
+        if c_srs.is_null() {
+            return Err(self.api.last_null_pointer_err("GDALGetSpatialRef"));
+        }
+        // GDALGetSpatialRef returns a borrowed reference — clone it via 
OSRClone.
+        unsafe { SpatialRef::from_c_srs_clone(self.api, c_srs) }
+    }
+
+    /// Set the spatial reference.
+    pub fn set_spatial_ref(&self, srs: &SpatialRef) -> Result<()> {
+        let rv =
+            unsafe { call_gdal_api!(self.api, GDALSetSpatialRef, 
self.c_dataset, srs.c_srs()) };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Create a copy of this dataset to a new file using the given driver.
+    pub fn create_copy(
+        &self,
+        driver: &Driver,
+        filename: &str,
+        options: &[&str],
+    ) -> Result<Dataset> {
+        let c_filename = CString::new(filename)?;
+        let csl = CslStringList::try_from_iter(options.iter().copied())?;
+
+        let c_ds = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALCreateCopy,
+                driver.c_driver(),
+                c_filename.as_ptr(),
+                self.c_dataset,
+                0, // bStrict
+                csl.as_ptr(),
+                ptr::null_mut(),
+                ptr::null_mut()
+            )
+        };
+        if c_ds.is_null() {
+            return Err(self.api.last_cpl_err(CE_Failure as u32));
+        }
+        Ok(Dataset::new(self.api, c_ds))
+    }
+
+    /// Create a new vector layer.
+    pub fn create_layer(&self, options: LayerOptions<'_>) -> Result<Layer<'_>> 
{
+        let c_name = CString::new(options.name)?;
+        let c_srs = options.srs.map_or(ptr::null_mut(), |s| s.c_srs());
+
+        let csl = 
CslStringList::try_from_iter(options.options.unwrap_or(&[]).iter().copied())?;
+
+        let c_layer = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALDatasetCreateLayer,
+                self.c_dataset,
+                c_name.as_ptr(),
+                c_srs,
+                options.ty,
+                csl.as_ptr()
+            )
+        };
+        if c_layer.is_null() {
+            return 
Err(self.api.last_null_pointer_err("GDALDatasetCreateLayer"));
+        }
+        Ok(Layer::new(self.api, c_layer, self))
+    }
+
+    /// Get the GDAL API reference.
+    pub fn api(&self) -> &'static GdalApi {
+        self.api
+    }
+
+    /// Open a dataset using a `DatasetOptions` struct (georust-compatible 
convenience).
+    pub fn open_ex_with_options(
+        api: &'static GdalApi,
+        path: &str,
+        options: DatasetOptions<'_>,
+    ) -> Result<Self> {
+        Self::open_ex(
+            api,
+            path,
+            options.open_flags,
+            options.allowed_drivers,
+            options.open_options,
+            options.sibling_files,
+        )
+    }
+
+    /// Add a band backed by an existing memory buffer.
+    /// Pass `DATAPOINTER`, `PIXELOFFSET`, and `LINEOFFSET` to `GDALAddBand`.
+    ///
+    /// # Safety
+    ///
+    /// `data_ptr` must point to valid band data that outlives this dataset.
+    pub unsafe fn add_band_with_data(
+        &self,
+        data_type: RustGdalDataType,
+        data_ptr: *const u8,
+        pixel_offset: Option<i64>,
+        line_offset: Option<i64>,
+    ) -> Result<()> {
+        let data_pointer = format!("DATAPOINTER={data_ptr:p}");
+
+        let mut options = CslStringList::with_capacity(3);
+        options.add_string(&data_pointer)?;
+
+        if let Some(pixel) = pixel_offset {
+            options.set_name_value("PIXELOFFSET", &pixel.to_string())?;
+        }
+
+        if let Some(line) = line_offset {
+            options.set_name_value("LINEOFFSET", &line.to_string())?;
+        }
+
+        let err = call_gdal_api!(
+            self.api,
+            GDALAddBand,
+            self.c_dataset,
+            data_type.to_c(),
+            options.as_ptr()
+        );
+        if err != CE_None {
+            return Err(self.api.last_cpl_err(err as u32));
+        }
+        Ok(())
+    }
+}
+
+/// Options for creating a vector layer.
+pub struct LayerOptions<'a> {
+    pub name: &'a str,
+    pub srs: Option<&'a SpatialRef>,
+    pub ty: OGRwkbGeometryType,
+    /// Additional driver-specific options, in the form `"name=value"`.
+    pub options: Option<&'a [&'a str]>,
+}
+
+impl Default for LayerOptions<'_> {
+    fn default() -> Self {
+        Self {
+            name: "",
+            srs: None,
+            ty: OGRwkbGeometryType::wkbUnknown,
+            options: None,
+        }
+    }
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+    use gdal_sys::{GDALDatasetGetLayer, GDALDatasetGetLayerCount};
+
+    use crate::driver::DriverManager;
+    use crate::gdal_dyn_bindgen::{OGRwkbGeometryType, GDAL_OF_READONLY, 
GDAL_OF_VECTOR};
+    use crate::global::with_global_gdal_api;
+    use crate::vector::layer::Layer;
+    use crate::vsi::unlink_mem_file;
+
+    use super::{Dataset, LayerOptions};
+
+    #[test]
+    fn test_geo_transform_roundtrip() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create("", 256, 256, 1).unwrap();
+
+            let gt = [0.0, 1.0, 0.0, 0.0, 0.0, -1.0];
+            ds.set_geo_transform(&gt).unwrap();
+            let got = ds.geo_transform().unwrap();
+            assert_eq!(gt, got);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_geo_transform_unset() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create("", 256, 256, 1).unwrap();
+
+            // MEM driver without an explicit set_geo_transform returns an 
error
+            assert!(ds.geo_transform().is_err());
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_set_projection_roundtrip() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create("", 256, 256, 1).unwrap();
+
+            let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 
84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"#;
+            ds.set_projection(wkt).unwrap();
+            let got = ds.projection();
+            // The returned WKT may be reformatted by GDAL, so just check it 
contains WGS 84
+            assert!(got.contains("WGS 84"), "Expected WGS 84 in: {got}");
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_dataset_raster_count() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+
+            let ds1 = driver.create("", 64, 64, 1).unwrap();
+            assert_eq!(ds1.raster_count(), 1);
+
+            let ds3 = driver.create("", 64, 64, 3).unwrap();
+            assert_eq!(ds3.raster_count(), 3);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_dataset_raster_size() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create("", 123, 456, 1).unwrap();
+            assert_eq!(ds.raster_size(), (123, 456));
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_create_vector_layer() {
+        with_global_gdal_api(|api| {
+            let path = "/vsimem/test_dataset_create_vector_layer.gpkg";
+            let driver = DriverManager::get_driver_by_name(api, 
"GPKG").unwrap();
+            let dataset = driver.create_vector_only(path).unwrap();
+
+            let layer = dataset
+                .create_layer(LayerOptions {
+                    name: "points",
+                    srs: None,
+                    ty: OGRwkbGeometryType::wkbPoint,
+                    options: None,
+                })
+                .unwrap();
+
+            assert_eq!(dataset.raster_count(), 0);
+            assert!(!layer.c_layer().is_null());
+            assert_eq!(layer.feature_count(true), 0);
+
+            unlink_mem_file(api, path).unwrap();
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_open_vector_dataset_with_open_ex() {
+        with_global_gdal_api(|api| {
+            let path = "/vsimem/test_dataset_open_vector.gpkg";
+            let driver = DriverManager::get_driver_by_name(api, 
"GPKG").unwrap();
+            {
+                let dataset = driver.create_vector_only(path).unwrap();
+
+                dataset
+                    .create_layer(LayerOptions {
+                        name: "points",
+                        srs: None,
+                        ty: OGRwkbGeometryType::wkbPoint,
+                        options: None,
+                    })
+                    .unwrap();
+            }
+
+            let reopened = Dataset::open_ex(
+                api,
+                path,
+                GDAL_OF_VECTOR | GDAL_OF_READONLY,
+                None,
+                None,
+                None,
+            )
+            .unwrap();
+
+            let layer_count = unsafe { 
GDALDatasetGetLayerCount(reopened.c_dataset()) };
+            assert_eq!(layer_count, 1);
+
+            let c_layer = unsafe { GDALDatasetGetLayer(reopened.c_dataset(), 
0) };
+            assert!(!c_layer.is_null());
+
+            let reopened_layer = Layer::new(api, c_layer, &reopened);
+            assert_eq!(reopened_layer.feature_count(true), 0);
+
+            unlink_mem_file(api, path).unwrap();
+        })
+        .unwrap();
+    }
+}
diff --git a/c/sedona-gdal/src/driver.rs b/c/sedona-gdal/src/driver.rs
new file mode 100644
index 00000000..cced1754
--- /dev/null
+++ b/c/sedona-gdal/src/driver.rs
@@ -0,0 +1,242 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/driver.rs>.
+//! Original code is licensed under MIT.
+
+use std::ffi::CString;
+use std::ptr;
+
+use crate::dataset::Dataset;
+use crate::errors::Result;
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::raster::types::GdalDataType as RustGdalDataType;
+use crate::raster::types::GdalType;
+
+/// A GDAL driver.
+pub struct Driver {
+    api: &'static GdalApi,
+    c_driver: GDALDriverH,
+}
+
+impl Driver {
+    /// Wrap an existing C driver handle.
+    ///
+    /// # Safety
+    ///
+    /// The caller must ensure the handle is valid.
+    pub unsafe fn from_c_driver(api: &'static GdalApi, c_driver: GDALDriverH) 
-> Self {
+        Self { api, c_driver }
+    }
+
+    /// Return the raw C driver handle.
+    pub fn c_driver(&self) -> GDALDriverH {
+        self.c_driver
+    }
+
+    /// Create a new raster dataset (with u8 band type).
+    pub fn create(
+        &self,
+        filename: &str,
+        size_x: usize,
+        size_y: usize,
+        bands: usize,
+    ) -> Result<Dataset> {
+        self.create_with_band_type::<u8>(filename, size_x, size_y, bands)
+    }
+
+    /// Create a new raster dataset with a specific band type.
+    pub fn create_with_band_type<T: GdalType>(
+        &self,
+        filename: &str,
+        size_x: usize,
+        size_y: usize,
+        bands: usize,
+    ) -> Result<Dataset> {
+        let c_filename = CString::new(filename)?;
+        let x: i32 = size_x.try_into()?;
+        let y: i32 = size_y.try_into()?;
+        let b: i32 = bands.try_into()?;
+        let c_ds = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALCreate,
+                self.c_driver,
+                c_filename.as_ptr(),
+                x,
+                y,
+                b,
+                T::gdal_ordinal(),
+                ptr::null_mut()
+            )
+        };
+        if c_ds.is_null() {
+            return Err(self.api.last_cpl_err(CE_Failure as u32));
+        }
+        Ok(Dataset::new(self.api, c_ds))
+    }
+
+    /// Create a new raster dataset with a runtime data type.
+    ///
+    /// Unlike [`create_with_band_type`](Self::create_with_band_type), this 
accepts a
+    /// [`GdalDataType`](RustGdalDataType) enum value instead of a 
compile-time generic,
+    /// which is useful when the data type is only known at runtime.
+    pub fn create_with_data_type(
+        &self,
+        filename: &str,
+        size_x: usize,
+        size_y: usize,
+        bands: usize,
+        data_type: RustGdalDataType,
+    ) -> Result<Dataset> {
+        let c_filename = CString::new(filename)?;
+        let x: i32 = size_x.try_into()?;
+        let y: i32 = size_y.try_into()?;
+        let b: i32 = bands.try_into()?;
+        let c_ds = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALCreate,
+                self.c_driver,
+                c_filename.as_ptr(),
+                x,
+                y,
+                b,
+                data_type.to_c(),
+                ptr::null_mut()
+            )
+        };
+        if c_ds.is_null() {
+            return Err(self.api.last_cpl_err(CE_Failure as u32));
+        }
+        Ok(Dataset::new(self.api, c_ds))
+    }
+
+    /// Create a new dataset (vector-only, no raster bands).
+    pub fn create_vector_only(&self, filename: &str) -> Result<Dataset> {
+        let c_filename = CString::new(filename)?;
+        let c_ds = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALCreate,
+                self.c_driver,
+                c_filename.as_ptr(),
+                1,
+                1,
+                0,
+                GDALDataType::GDT_Unknown,
+                ptr::null_mut()
+            )
+        };
+        if c_ds.is_null() {
+            return Err(self.api.last_cpl_err(CE_Failure as u32));
+        }
+        Ok(Dataset::new(self.api, c_ds))
+    }
+}
+
+/// Driver manager for looking up drivers by name.
+pub struct DriverManager;
+
+impl DriverManager {
+    pub fn get_driver_by_name(api: &'static GdalApi, name: &str) -> 
Result<Driver> {
+        let c_name = CString::new(name)?;
+        let c_driver = unsafe { call_gdal_api!(api, GDALGetDriverByName, 
c_name.as_ptr()) };
+        if c_driver.is_null() {
+            return Err(api.last_null_pointer_err("GDALGetDriverByName"));
+        }
+        Ok(Driver { api, c_driver })
+    }
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+    use crate::driver::DriverManager;
+    use crate::errors::GdalError;
+    use crate::global::with_global_gdal_api;
+    use crate::raster::types::GdalDataType;
+
+    #[test]
+    fn test_get_driver_by_name() {
+        with_global_gdal_api(|api| {
+            let gtiff = DriverManager::get_driver_by_name(api, 
"GTiff").unwrap();
+            assert!(!gtiff.c_driver().is_null());
+            let mem = DriverManager::get_driver_by_name(api, "MEM").unwrap();
+            assert!(!mem.c_driver().is_null());
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_get_driver_by_name_invalid() {
+        with_global_gdal_api(|api| {
+            let err = DriverManager::get_driver_by_name(api, "NO_SUCH_DRIVER");
+            assert!(matches!(err, Err(GdalError::NullPointer { .. })));
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_driver_create() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create("", 32, 16, 2).unwrap();
+            assert_eq!(ds.raster_size(), (32, 16));
+            assert_eq!(ds.raster_count(), 2);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_driver_create_with_band_type() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create_with_band_type::<u8>("", 10, 20, 
1).unwrap();
+            assert_eq!(ds.raster_count(), 1);
+            let ds = driver.create_with_band_type::<f32>("", 10, 20, 
2).unwrap();
+            assert_eq!(ds.raster_count(), 2);
+            let ds = driver.create_with_band_type::<i16>("", 10, 20, 
3).unwrap();
+            assert_eq!(ds.raster_count(), 3);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_driver_create_with_data_type() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver
+                .create_with_data_type("", 8, 8, 1, GdalDataType::UInt16)
+                .unwrap();
+            assert_eq!(ds.raster_count(), 1);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_driver_create_vector_only() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let ds = driver.create_vector_only("").unwrap();
+            assert_eq!(ds.raster_count(), 0);
+            assert_eq!(ds.raster_size(), (1, 1));
+        })
+        .unwrap();
+    }
+}
diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs
index f344667e..3ff5d03d 100644
--- a/c/sedona-gdal/src/errors.rs
+++ b/c/sedona-gdal/src/errors.rs
@@ -63,6 +63,9 @@ pub enum GdalError {
 
     #[error(transparent)]
     IntConversionError(#[from] TryFromIntError),
+
+    #[error("Buffer length {0} does not match raster size {1:?}")]
+    BufferSizeMismatch(usize, (usize, usize)),
 }
 
 pub type Result<T> = std::result::Result<T, GdalError>;
diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs
index 0646a241..c331f5df 100644
--- a/c/sedona-gdal/src/lib.rs
+++ b/c/sedona-gdal/src/lib.rs
@@ -29,6 +29,8 @@ pub mod global;
 // --- High-level wrappers ---
 pub mod config;
 pub mod cpl;
+pub mod dataset;
+pub mod driver;
 pub mod geo_transform;
 pub mod raster;
 pub mod spatial_ref;
diff --git a/c/sedona-gdal/src/raster.rs b/c/sedona-gdal/src/raster.rs
index 1ddc9b2e..389d9d73 100644
--- a/c/sedona-gdal/src/raster.rs
+++ b/c/sedona-gdal/src/raster.rs
@@ -15,4 +15,5 @@
 // specific language governing permissions and limitations
 // under the License.
 
+pub mod rasterband;
 pub mod types;
diff --git a/c/sedona-gdal/src/raster/rasterband.rs 
b/c/sedona-gdal/src/raster/rasterband.rs
new file mode 100644
index 00000000..9e537669
--- /dev/null
+++ b/c/sedona-gdal/src/raster/rasterband.rs
@@ -0,0 +1,372 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/raster/rasterband.rs>.
+//! Original code is licensed under MIT.
+
+use std::marker::PhantomData;
+
+use crate::dataset::Dataset;
+use crate::errors::{GdalError, Result};
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::raster::types::{Buffer, GdalType, ResampleAlg};
+use crate::{gdal_dyn_bindgen::*, raster::types::GdalDataType};
+
+/// A raster band of a dataset.
+pub struct RasterBand<'a> {
+    api: &'static GdalApi,
+    c_rasterband: GDALRasterBandH,
+    _dataset: PhantomData<&'a Dataset>,
+}
+
+impl<'a> RasterBand<'a> {
+    pub(crate) fn new(
+        api: &'static GdalApi,
+        c_rasterband: GDALRasterBandH,
+        _dataset: &'a Dataset,
+    ) -> Self {
+        Self {
+            api,
+            c_rasterband,
+            _dataset: PhantomData,
+        }
+    }
+
+    /// Return the raw C raster band handle.
+    pub fn c_rasterband(&self) -> GDALRasterBandH {
+        self.c_rasterband
+    }
+
+    /// Read a window of this band into a typed buffer.
+    /// If `e_resample_alg` is `None`, use nearest-neighbour resampling.
+    pub fn read_as<T: GdalType + Copy>(
+        &self,
+        window: (isize, isize),
+        window_size: (usize, usize),
+        size: (usize, usize),
+        e_resample_alg: Option<ResampleAlg>,
+    ) -> Result<Buffer<T>> {
+        let len = size.0 * size.1;
+        // Safety: all GdalType implementations are numeric primitives (u8, 
i8, u16, ..., f64),
+        // for which zeroed memory is a valid bit pattern.
+        let mut data: Vec<T> = vec![unsafe { std::mem::zeroed() }; len];
+
+        let resample_alg = 
e_resample_alg.unwrap_or(ResampleAlg::NearestNeighbour);
+        let mut extra_arg = GDALRasterIOExtraArg {
+            eResampleAlg: resample_alg.to_gdal(),
+            ..GDALRasterIOExtraArg::default()
+        };
+
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALRasterIOEx,
+                self.c_rasterband,
+                GF_Read,
+                i32::try_from(window.0)?,
+                i32::try_from(window.1)?,
+                i32::try_from(window_size.0)?,
+                i32::try_from(window_size.1)?,
+                data.as_mut_ptr() as *mut std::ffi::c_void,
+                i32::try_from(size.0)?,
+                i32::try_from(size.1)?,
+                T::gdal_ordinal(),
+                0, // nPixelSpace (auto)
+                0, // nLineSpace (auto)
+                &mut extra_arg
+            )
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+
+        Ok(Buffer::new(size, data))
+    }
+
+    /// Write a buffer to this raster band.
+    pub fn write<T: GdalType + Copy>(
+        &self,
+        window: (isize, isize),
+        window_size: (usize, usize),
+        buffer: &mut Buffer<T>,
+    ) -> Result<()> {
+        let expected_len = buffer.shape.0 * buffer.shape.1;
+        if buffer.data.len() != expected_len {
+            return Err(GdalError::BufferSizeMismatch(
+                buffer.data.len(),
+                buffer.shape,
+            ));
+        }
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALRasterIO,
+                self.c_rasterband,
+                GF_Write,
+                i32::try_from(window.0)?,
+                i32::try_from(window.1)?,
+                i32::try_from(window_size.0)?,
+                i32::try_from(window_size.1)?,
+                buffer.data.as_mut_ptr() as *mut std::ffi::c_void,
+                i32::try_from(buffer.shape.0)?,
+                i32::try_from(buffer.shape.1)?,
+                T::gdal_ordinal(),
+                0, // nPixelSpace (auto)
+                0  // nLineSpace (auto)
+            )
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Fetch this band's data type.
+    pub fn band_type(&self) -> GdalDataType {
+        
GdalDataType::from_c(self.c_band_type()).unwrap_or(GdalDataType::Unknown)
+    }
+
+    /// Fetch this band's raw GDAL data type.
+    pub fn c_band_type(&self) -> GDALDataType {
+        unsafe { call_gdal_api!(self.api, GDALGetRasterDataType, 
self.c_rasterband) }
+    }
+
+    /// Fetch band size as `(x_size, y_size)`.
+    pub fn size(&self) -> (usize, usize) {
+        let x = unsafe { call_gdal_api!(self.api, GDALGetRasterBandXSize, 
self.c_rasterband) };
+        let y = unsafe { call_gdal_api!(self.api, GDALGetRasterBandYSize, 
self.c_rasterband) };
+        (x as usize, y as usize)
+    }
+
+    /// Fetch the natural block size as `(x_size, y_size)`.
+    pub fn block_size(&self) -> (usize, usize) {
+        let mut x: i32 = 0;
+        let mut y: i32 = 0;
+        unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALGetBlockSize,
+                self.c_rasterband,
+                &mut x,
+                &mut y
+            )
+        };
+        (x as usize, y as usize)
+    }
+
+    /// Fetch the band's nodata value.
+    /// Return `None` if no nodata value is set.
+    pub fn no_data_value(&self) -> Option<f64> {
+        let mut success: i32 = 0;
+        let value = unsafe {
+            call_gdal_api!(
+                self.api,
+                GDALGetRasterNoDataValue,
+                self.c_rasterband,
+                &mut success
+            )
+        };
+        if success != 0 {
+            Some(value)
+        } else {
+            None
+        }
+    }
+
+    /// Set the band's nodata value.
+    /// Pass `None` to clear any existing nodata value.
+    pub fn set_no_data_value(&self, value: Option<f64>) -> Result<()> {
+        let rv = if let Some(val) = value {
+            unsafe { call_gdal_api!(self.api, GDALSetRasterNoDataValue, 
self.c_rasterband, val) }
+        } else {
+            unsafe { call_gdal_api!(self.api, GDALDeleteRasterNoDataValue, 
self.c_rasterband) }
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Set the band's nodata value as `u64`.
+    /// Pass `None` to clear any existing nodata value.
+    pub fn set_no_data_value_u64(&self, value: Option<u64>) -> Result<()> {
+        let rv = if let Some(val) = value {
+            unsafe {
+                call_gdal_api!(
+                    self.api,
+                    GDALSetRasterNoDataValueAsUInt64,
+                    self.c_rasterband,
+                    val
+                )
+            }
+        } else {
+            unsafe { call_gdal_api!(self.api, GDALDeleteRasterNoDataValue, 
self.c_rasterband) }
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Set the band's nodata value as `i64`.
+    /// Pass `None` to clear any existing nodata value.
+    pub fn set_no_data_value_i64(&self, value: Option<i64>) -> Result<()> {
+        let rv = if let Some(val) = value {
+            unsafe {
+                call_gdal_api!(
+                    self.api,
+                    GDALSetRasterNoDataValueAsInt64,
+                    self.c_rasterband,
+                    val
+                )
+            }
+        } else {
+            unsafe { call_gdal_api!(self.api, GDALDeleteRasterNoDataValue, 
self.c_rasterband) }
+        };
+        if rv != CE_None {
+            return Err(self.api.last_cpl_err(rv as u32));
+        }
+        Ok(())
+    }
+
+    /// Get the GDAL API reference.
+    pub fn api(&self) -> &'static GdalApi {
+        self.api
+    }
+}
+
+/// Return the actual block size for a block index.
+/// Clamp edge blocks to the raster extent.
+pub fn actual_block_size(
+    band: &RasterBand<'_>,
+    block_index: (usize, usize),
+) -> Result<(usize, usize)> {
+    let (block_x, block_y) = band.block_size();
+    let (raster_x, raster_y) = band.size();
+    let x_off = block_index.0 * block_x;
+    let y_off = block_index.1 * block_y;
+    if x_off >= raster_x || y_off >= raster_y {
+        return Err(GdalError::BadArgument(format!(
+            "block index ({}, {}) is out of bounds for raster size ({}, {})",
+            block_index.0, block_index.1, raster_x, raster_y
+        )));
+    }
+    let actual_x = if x_off + block_x > raster_x {
+        raster_x - x_off
+    } else {
+        block_x
+    };
+    let actual_y = if y_off + block_y > raster_y {
+        raster_y - y_off
+    } else {
+        block_y
+    };
+    Ok((actual_x, actual_y))
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+    use crate::dataset::Dataset;
+    use crate::driver::DriverManager;
+    use crate::gdal_dyn_bindgen::*;
+    use crate::global::with_global_gdal_api;
+    use crate::raster::types::ResampleAlg;
+
+    fn fixture(name: &str) -> String {
+        sedona_testing::data::test_raster(name).unwrap()
+    }
+
+    #[test]
+    fn test_read_raster() {
+        with_global_gdal_api(|api| {
+            let path = fixture("tinymarble.tif");
+            let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, 
None, None).unwrap();
+            let rb = dataset.rasterband(1).unwrap();
+            let rv = rb.read_as::<u8>((20, 30), (2, 3), (2, 3), None).unwrap();
+            assert_eq!(rv.shape, (2, 3));
+            assert_eq!(rv.data(), [7, 7, 7, 10, 8, 12]);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_read_raster_with_default_resample() {
+        with_global_gdal_api(|api| {
+            let path = fixture("tinymarble.tif");
+            let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, 
None, None).unwrap();
+            let rb = dataset.rasterband(1).unwrap();
+            let rv = rb.read_as::<u8>((20, 30), (4, 4), (2, 2), None).unwrap();
+            assert_eq!(rv.shape, (2, 2));
+            // Default is NearestNeighbour; exact values are 
GDAL-version-dependent
+            // when downsampling from 4x4 to 2x2. Just verify shape and 
non-emptiness.
+            assert_eq!(rv.data().len(), 4);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_read_raster_with_average_resample() {
+        with_global_gdal_api(|api| {
+            let path = fixture("tinymarble.tif");
+            let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, 
None, None).unwrap();
+            let rb = dataset.rasterband(1).unwrap();
+            let rv = rb
+                .read_as::<u8>((20, 30), (4, 4), (2, 2), 
Some(ResampleAlg::Average))
+                .unwrap();
+            assert_eq!(rv.shape, (2, 2));
+            // Average resampling; exact values are GDAL-version-dependent, so 
just
+            // verify that the downsampled result has the expected shape and 
length.
+            assert_eq!(rv.data().len(), 4);
+        })
+        .unwrap();
+    }
+
+    #[test]
+    fn test_get_no_data_value() {
+        with_global_gdal_api(|api| {
+            // tinymarble.tif has no nodata
+            let path = fixture("tinymarble.tif");
+            let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, 
None, None).unwrap();
+            let rb = dataset.rasterband(1).unwrap();
+            assert!(rb.no_data_value().is_none());
+
+            // labels.tif has nodata=255
+            let path = fixture("labels.tif");
+            let dataset = Dataset::open_ex(api, &path, GDAL_OF_READONLY, None, 
None, None).unwrap();
+            let rb = dataset.rasterband(1).unwrap();
+            assert_eq!(rb.no_data_value(), Some(255.0));
+        })
+        .unwrap();
+    }
+
+    #[test]
+    #[allow(clippy::float_cmp)]
+    fn test_set_no_data_value() {
+        with_global_gdal_api(|api| {
+            let driver = DriverManager::get_driver_by_name(api, 
"MEM").unwrap();
+            let dataset = driver.create("", 20, 10, 1).unwrap();
+            let rasterband = dataset.rasterband(1).unwrap();
+            assert_eq!(rasterband.no_data_value(), None);
+            assert!(rasterband.set_no_data_value(Some(1.23)).is_ok());
+            assert_eq!(rasterband.no_data_value(), Some(1.23));
+            assert!(rasterband.set_no_data_value(None).is_ok());
+            assert_eq!(rasterband.no_data_value(), None);
+        })
+        .unwrap();
+    }
+}
diff --git a/c/sedona-gdal/src/vector.rs b/c/sedona-gdal/src/vector.rs
index 10c038ed..52ff4e1b 100644
--- a/c/sedona-gdal/src/vector.rs
+++ b/c/sedona-gdal/src/vector.rs
@@ -15,4 +15,6 @@
 // specific language governing permissions and limitations
 // under the License.
 
+pub mod feature;
 pub mod geometry;
+pub mod layer;
diff --git a/c/sedona-gdal/src/vector/feature.rs 
b/c/sedona-gdal/src/vector/feature.rs
new file mode 100644
index 00000000..a80648b1
--- /dev/null
+++ b/c/sedona-gdal/src/vector/feature.rs
@@ -0,0 +1,213 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/vector/feature.rs>.
+//! Original code is licensed under MIT.
+
+use std::ffi::CString;
+use std::marker::PhantomData;
+
+use crate::errors::{GdalError, Result};
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::vector::geometry::Envelope;
+
+/// An OGR feature.
+pub struct Feature<'a> {
+    api: &'static GdalApi,
+    c_feature: OGRFeatureH,
+    _lifetime: PhantomData<&'a ()>,
+}
+
+impl Drop for Feature<'_> {
+    fn drop(&mut self) {
+        if !self.c_feature.is_null() {
+            unsafe { call_gdal_api!(self.api, OGR_F_Destroy, self.c_feature) };
+        }
+    }
+}
+
+impl<'a> Feature<'a> {
+    pub(crate) fn new(api: &'static GdalApi, c_feature: OGRFeatureH) -> Self {
+        Self {
+            api,
+            c_feature,
+            _lifetime: PhantomData,
+        }
+    }
+
+    /// Fetch the feature geometry.
+    /// The returned geometry is borrowed; return `None` if no geometry is set.
+    pub fn geometry(&self) -> Option<BorrowedGeometry<'_>> {
+        let c_geom = unsafe { call_gdal_api!(self.api, OGR_F_GetGeometryRef, 
self.c_feature) };
+        if c_geom.is_null() {
+            None
+        } else {
+            Some(BorrowedGeometry {
+                api: self.api,
+                c_geom,
+                _lifetime: PhantomData,
+            })
+        }
+    }
+
+    /// Fetch the index of a field by name.
+    /// Return an error if the field is not found.
+    pub fn field_index(&self, name: &str) -> Result<i32> {
+        let c_name = CString::new(name)?;
+        let idx = unsafe {
+            call_gdal_api!(
+                self.api,
+                OGR_F_GetFieldIndex,
+                self.c_feature,
+                c_name.as_ptr()
+            )
+        };
+        if idx < 0 {
+            return Err(GdalError::BadArgument(format!("field '{name}' not 
found")));
+        }
+        Ok(idx)
+    }
+
+    /// Fetch a field value as `f64`.
+    pub fn field_as_double(&self, field_index: i32) -> f64 {
+        unsafe {
+            call_gdal_api!(
+                self.api,
+                OGR_F_GetFieldAsDouble,
+                self.c_feature,
+                field_index
+            )
+        }
+    }
+
+    /// Fetch a field value as `i32`.
+    /// Return `None` if the field is unset or null.
+    pub fn field_as_integer(&self, field_index: i32) -> Option<i32> {
+        let is_set = unsafe {
+            call_gdal_api!(
+                self.api,
+                OGR_F_IsFieldSetAndNotNull,
+                self.c_feature,
+                field_index
+            )
+        };
+        if is_set != 0 {
+            Some(unsafe {
+                call_gdal_api!(
+                    self.api,
+                    OGR_F_GetFieldAsInteger,
+                    self.c_feature,
+                    field_index
+                )
+            })
+        } else {
+            None
+        }
+    }
+}
+
+/// A geometry borrowed from a feature (not owned — will NOT be destroyed).
+pub struct BorrowedGeometry<'a> {
+    api: &'static GdalApi,
+    c_geom: OGRGeometryH,
+    _lifetime: PhantomData<&'a ()>,
+}
+
+impl<'a> BorrowedGeometry<'a> {
+    /// Return the raw C geometry handle.
+    pub fn c_geometry(&self) -> OGRGeometryH {
+        self.c_geom
+    }
+
+    /// Export to ISO WKB.
+    pub fn wkb(&self) -> Result<Vec<u8>> {
+        let size = unsafe { call_gdal_api!(self.api, OGR_G_WkbSize, 
self.c_geom) };
+        if size < 0 {
+            return Err(GdalError::BadArgument(format!(
+                "OGR_G_WkbSize returned negative size: {size}"
+            )));
+        }
+        let mut buf = vec![0u8; size as usize];
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                OGR_G_ExportToIsoWkb,
+                self.c_geom,
+                wkbNDR,
+                buf.as_mut_ptr()
+            )
+        };
+        if rv != OGRERR_NONE {
+            return Err(GdalError::OgrError {
+                err: rv,
+                method_name: "OGR_G_ExportToIsoWkb",
+            });
+        }
+        Ok(buf)
+    }
+
+    /// Fetch the 2D envelope of this geometry.
+    pub fn envelope(&self) -> Envelope {
+        let mut env = OGREnvelope {
+            MinX: 0.0,
+            MaxX: 0.0,
+            MinY: 0.0,
+            MaxY: 0.0,
+        };
+        unsafe { call_gdal_api!(self.api, OGR_G_GetEnvelope, self.c_geom, &mut 
env) };
+        Envelope {
+            MinX: env.MinX,
+            MaxX: env.MaxX,
+            MinY: env.MinY,
+            MaxY: env.MaxY,
+        }
+    }
+}
+
+/// An OGR field definition.
+pub struct FieldDefn {
+    api: &'static GdalApi,
+    c_field_defn: OGRFieldDefnH,
+}
+
+impl Drop for FieldDefn {
+    fn drop(&mut self) {
+        if !self.c_field_defn.is_null() {
+            unsafe { call_gdal_api!(self.api, OGR_Fld_Destroy, 
self.c_field_defn) };
+        }
+    }
+}
+
+impl FieldDefn {
+    /// Create a new field definition.
+    pub fn new(api: &'static GdalApi, name: &str, field_type: OGRFieldType) -> 
Result<Self> {
+        let c_name = CString::new(name)?;
+        let c_field_defn =
+            unsafe { call_gdal_api!(api, OGR_Fld_Create, c_name.as_ptr(), 
field_type) };
+        if c_field_defn.is_null() {
+            return Err(api.last_null_pointer_err("OGR_Fld_Create"));
+        }
+        Ok(Self { api, c_field_defn })
+    }
+
+    /// Return the raw C handle.
+    pub fn c_field_defn(&self) -> OGRFieldDefnH {
+        self.c_field_defn
+    }
+}
diff --git a/c/sedona-gdal/src/vector/layer.rs 
b/c/sedona-gdal/src/vector/layer.rs
new file mode 100644
index 00000000..8111f231
--- /dev/null
+++ b/c/sedona-gdal/src/vector/layer.rs
@@ -0,0 +1,208 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/vector/layer.rs>.
+//! Original code is licensed under MIT.
+
+use std::marker::PhantomData;
+
+use crate::dataset::Dataset;
+use crate::errors::{GdalError, Result};
+use crate::gdal_api::{call_gdal_api, GdalApi};
+use crate::gdal_dyn_bindgen::*;
+use crate::vector::feature::{Feature, FieldDefn};
+
+/// An OGR layer (borrowed from a Dataset).
+pub struct Layer<'a> {
+    api: &'static GdalApi,
+    c_layer: OGRLayerH,
+    _dataset: PhantomData<&'a Dataset>,
+}
+
+impl<'a> Layer<'a> {
+    pub(crate) fn new(api: &'static GdalApi, c_layer: OGRLayerH, _dataset: &'a 
Dataset) -> Self {
+        Self {
+            api,
+            c_layer,
+            _dataset: PhantomData,
+        }
+    }
+
+    /// Return the raw C layer handle.
+    pub fn c_layer(&self) -> OGRLayerH {
+        self.c_layer
+    }
+
+    /// Reset reading to the first feature.
+    pub fn reset_reading(&self) {
+        unsafe { call_gdal_api!(self.api, OGR_L_ResetReading, self.c_layer) };
+    }
+
+    /// Fetch the next feature from the current read cursor.
+    /// Return `None` when no more features are available.
+    pub fn next_feature(&self) -> Option<Feature<'_>> {
+        let c_feature = unsafe { call_gdal_api!(self.api, 
OGR_L_GetNextFeature, self.c_layer) };
+        if c_feature.is_null() {
+            None
+        } else {
+            Some(Feature::new(self.api, c_feature))
+        }
+    }
+
+    /// Create a field in this layer.
+    /// Allow the driver to approximate the definition if needed.
+    pub fn create_field(&self, field_defn: &FieldDefn) -> Result<()> {
+        let rv = unsafe {
+            call_gdal_api!(
+                self.api,
+                OGR_L_CreateField,
+                self.c_layer,
+                field_defn.c_field_defn(),
+                1 // bApproxOK
+            )
+        };
+        if rv != OGRERR_NONE {
+            return Err(GdalError::OgrError {
+                err: rv,
+                method_name: "OGR_L_CreateField",
+            });
+        }
+        Ok(())
+    }
+
+    /// Fetch the feature count for this layer.
+    /// Return `-1` if the count is unknown and `force` is `false`.
+    pub fn feature_count(&self, force: bool) -> i64 {
+        unsafe {
+            call_gdal_api!(
+                self.api,
+                OGR_L_GetFeatureCount,
+                self.c_layer,
+                if force { 1 } else { 0 }
+            )
+        }
+    }
+
+    /// Iterate over features from the start of the layer.
+    /// This resets the layer read cursor before iteration.
+    pub fn features(&mut self) -> FeatureIterator<'_> {
+        self.reset_reading();
+        FeatureIterator { layer: self }
+    }
+}
+
+/// Iterator over features in a layer.
+pub struct FeatureIterator<'a> {
+    layer: &'a Layer<'a>,
+}
+
+impl<'a> Iterator for FeatureIterator<'a> {
+    type Item = Feature<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.layer.next_feature()
+    }
+}
+
+#[cfg(all(test, feature = "gdal-sys"))]
+mod tests {
+    use gdal_sys::{
+        GDALDatasetGetLayer, GDALDatasetGetLayerCount, OGR_F_Create, 
OGR_F_SetGeometry,
+        OGR_L_CreateFeature, OGR_L_GetLayerDefn,
+    };
+
+    use super::Layer;
+    use crate::dataset::{Dataset, LayerOptions};
+    use crate::driver::DriverManager;
+    use crate::gdal_dyn_bindgen::OGRwkbGeometryType;
+    use crate::global::with_global_gdal_api;
+    use crate::vector::geometry::Geometry;
+    use crate::vsi::unlink_mem_file;
+
+    #[test]
+    fn test_layer_iteration_and_reset() {
+        with_global_gdal_api(|api| {
+            let path = "/vsimem/test_layer_iteration.gpkg";
+            let driver = DriverManager::get_driver_by_name(api, 
"GPKG").unwrap();
+            let dataset = driver.create_vector_only(path).unwrap();
+
+            let layer = dataset
+                .create_layer(LayerOptions {
+                    name: "features",
+                    srs: None,
+                    ty: OGRwkbGeometryType::wkbPoint,
+                    options: None,
+                })
+                .unwrap();
+
+            let layer_defn = unsafe { OGR_L_GetLayerDefn(layer.c_layer()) };
+            assert!(!layer_defn.is_null());
+
+            for x in [1.0_f64, 2.0, 3.0] {
+                let feature = unsafe { OGR_F_Create(layer_defn) };
+                assert!(!feature.is_null());
+
+                let geometry = Geometry::from_wkt(api, &format!("POINT ({x} 
0)")).unwrap();
+                let set_geometry_err = unsafe { OGR_F_SetGeometry(feature, 
geometry.c_geometry()) };
+                assert_eq!(set_geometry_err, gdal_sys::OGRErr::OGRERR_NONE);
+
+                let create_feature_err = unsafe { 
OGR_L_CreateFeature(layer.c_layer(), feature) };
+                assert_eq!(create_feature_err, gdal_sys::OGRErr::OGRERR_NONE);
+
+                unsafe { gdal_sys::OGR_F_Destroy(feature) };
+            }
+
+            let write_count = unsafe { 
GDALDatasetGetLayerCount(dataset.c_dataset()) };
+            assert_eq!(write_count, 1);
+
+            let read_dataset = Dataset::open_ex(
+                api,
+                path,
+                crate::gdal_dyn_bindgen::GDAL_OF_VECTOR | 
crate::gdal_dyn_bindgen::GDAL_OF_READONLY,
+                None,
+                None,
+                None,
+            )
+            .unwrap();
+
+            let read_count = unsafe { 
GDALDatasetGetLayerCount(read_dataset.c_dataset()) };
+            assert_eq!(read_count, 1);
+
+            let c_layer = unsafe { 
GDALDatasetGetLayer(read_dataset.c_dataset(), 0) };
+            assert!(!c_layer.is_null());
+            let mut read_layer = Layer::new(api, c_layer, &read_dataset);
+
+            assert_eq!(read_layer.feature_count(true), 3);
+
+            let mut iter = read_layer.features();
+            assert!(iter.next().is_some());
+            assert!(iter.next().is_some());
+            assert!(iter.next().is_some());
+            assert!(iter.next().is_none());
+
+            read_layer.reset_reading();
+            assert!(read_layer.next_feature().is_some());
+
+            assert_eq!(read_layer.features().count(), 3);
+            assert_eq!(read_layer.features().count(), 3);
+
+            unlink_mem_file(api, path).unwrap();
+        })
+        .unwrap();
+    }
+}
diff --git a/c/sedona-gdal/src/vsi.rs b/c/sedona-gdal/src/vsi.rs
index 80022c7c..20a60e11 100644
--- a/c/sedona-gdal/src/vsi.rs
+++ b/c/sedona-gdal/src/vsi.rs
@@ -276,7 +276,7 @@ mod tests {
             let buffer = get_vsi_mem_file_buffer_owned(api, 
file_name).unwrap();
 
             assert!(buffer.is_empty());
-            assert_eq!(buffer.as_ref(), &[]);
+            assert_eq!(buffer.as_ref(), b"");
         })
         .unwrap();
     }

Reply via email to