ousia 2.0.1

Postgres ORM with native double-entry ledger, graph relations, and atomic money operations for Rust
Documentation
use super::PostgresAdapter;
use sqlx::Row;
use uuid::Uuid;

use crate::{
    adapters::{Error, GeoAdapter},
    query::GeoPoint,
};

#[async_trait::async_trait]
impl GeoAdapter for PostgresAdapter {
    async fn upsert_geo_points(
        &self,
        type_name: &str,
        object_id: Uuid,
        points: Vec<GeoPoint>,
    ) -> Result<(), Error> {
        if points.is_empty() {
            return Ok(());
        }

        let fields: Vec<&str> = points.iter().map(|p| p.field).collect();
        let lons: Vec<f64> = points.iter().map(|p| p.lon).collect();
        let lats: Vec<f64> = points.iter().map(|p| p.lat).collect();
        let hashes: Vec<&str> = points.iter().map(|p| p.hash.as_str()).collect();

        sqlx::query(
            r#"
            INSERT INTO public.object_geo (object_id, type, field, location, hash)
            SELECT $1, $2,
                   f.field,
                   ST_SetSRID(ST_MakePoint(f.lon, f.lat), 4326)::geography,
                   f.hash
            FROM UNNEST($3::text[], $4::float8[], $5::float8[], $6::text[])
                AS f(field, lon, lat, hash)
            -- Conflict target must include the partition key (type)
            -- because the partitioned table's PK is (type, object_id, field).
            ON CONFLICT (type, object_id, field) DO UPDATE
                SET location = EXCLUDED.location,
                    hash     = EXCLUDED.hash
            "#,
        )
        .bind(object_id)
        .bind(type_name)
        .bind(&fields)
        .bind(&lons)
        .bind(&lats)
        .bind(&hashes)
        .execute(&self.pool)
        .await
        .map_err(|e| Error::Storage(e.to_string()))?;

        Ok(())
    }

    async fn get_geo_hashes(&self, object_id: Uuid) -> Result<Vec<(String, String)>, Error> {
        let rows = sqlx::query(
            r#"
            SELECT field, hash FROM public.object_geo WHERE object_id = $1
            "#,
        )
        .bind(object_id)
        .fetch_all(&self.pool)
        .await
        .map_err(|e| Error::Storage(e.to_string()))?;

        Ok(rows
            .into_iter()
            .map(|row| {
                let field: String = row.try_get("field").unwrap();
                let hash: String = row.try_get("hash").unwrap();
                (field, hash)
            })
            .collect())
    }

    async fn delete_geo_fields(&self, object_id: Uuid, fields: Vec<String>) -> Result<(), Error> {
        if fields.is_empty() {
            return Ok(());
        }
        sqlx::query(
            r#"
            DELETE FROM public.object_geo WHERE object_id = $1 AND field = ANY($2)
            "#,
        )
        .bind(object_id)
        .bind(&fields)
        .execute(&self.pool)
        .await
        .map_err(|e| Error::Storage(e.to_string()))?;
        Ok(())
    }

    async fn delete_geo_for_object(&self, object_id: Uuid) -> Result<(), Error> {
        sqlx::query(
            r#"
            DELETE FROM public.object_geo WHERE object_id = $1
            "#,
        )
        .bind(object_id)
        .execute(&self.pool)
        .await
        .map_err(|e| Error::Storage(e.to_string()))?;
        Ok(())
    }
}