microrm 0.6.3

Lightweight ORM using sqlite as a backend
Documentation
use crate::{
    db::{StatementContext, Transaction},
    schema::{
        datum::{BorrowedDatumList, Datum, DatumVisitor},
        entity::{Entity, EntityID, EntityPart, EntityPartVisitor, EntityRef},
        relation::RelationData,
        Stored,
    },
    DBResult, Error,
};

use super::RelationNames;

struct PartNameVisitor<'a, E: Entity>(&'a mut String, &'a mut String, std::marker::PhantomData<E>);
impl<E: Entity> EntityPartVisitor for PartNameVisitor<'_, E> {
    type Entity = E;
    fn visit<EP: EntityPart>(&mut self) {
        // only care about datums that have representative columns
        if !EP::Datum::INTERNAL_DATUM {
            return;
        }

        if !self.0.is_empty() {
            self.0.push_str(", ");
            self.1.push_str(", ");
        }
        self.0.push('`');
        self.0.push_str(EP::part_name());
        self.0.push('`');
        self.1.push('?');
    }
}

struct PartBinder<'a, 'b, E: Entity>(
    &'a mut StatementContext<'b>,
    i32,
    std::marker::PhantomData<E>,
);
impl<E: Entity> EntityPartVisitor for PartBinder<'_, '_, E> {
    type Entity = E;
    fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) -> DBResult<()> {
        if EP::Datum::INTERNAL_DATUM {
            datum.bind_to(self.0, self.1)?;
            self.1 += 1;
        }
        Ok(())
    }
}

impl<E: Entity> DatumVisitor for PartBinder<'_, '_, E> {
    fn visit<ED: Datum>(&mut self, datum: &ED) -> DBResult<()> {
        if ED::INTERNAL_DATUM {
            datum.bind_to(self.0, self.1)?;
            self.1 += 1;
        }
        Ok(())
    }
}

pub(crate) fn insert_exact<E: Entity>(
    conn: &mut Transaction,
    value: &E,
    id: E::ID,
) -> DBResult<()> {
    struct InsertExactQuery<E: Entity>(std::marker::PhantomData<E>);

    conn.lease().with_prepared(
        std::any::TypeId::of::<InsertExactQuery<E>>(),
        || {
            let mut part_names = String::new();
            let mut placeholders = String::new();

            E::accept_part_visitor(&mut PartNameVisitor(
                &mut part_names,
                &mut placeholders,
                Default::default(),
            ));

            if part_names.is_empty() {
                part_names.clear();
                part_names.push_str("id");
                placeholders.clear();
                placeholders.push('?');
            } else {
                part_names = format!("`id`, {part_names}");
                placeholders = format!("?, {placeholders}");
            }

            Ok(format!(
                "INSERT INTO `{}` ({}) VALUES ({})",
                E::entity_name(),
                part_names,
                placeholders
            ))
        },
        |mut ctx| {
            id.bind_to(&mut ctx, 1)?;
            value.accept_part_visitor_ref(&mut PartBinder(&mut ctx, 2, Default::default()))?;

            ctx.run()?;
            Ok(())
        },
    )?;

    Ok(())
}

pub(crate) fn insert<E: Entity>(conn: &mut Transaction, value: &E) -> DBResult<E::ID> {
    struct InsertQuery<E: Entity>(std::marker::PhantomData<E>);

    conn.lease().with_prepared(
        std::any::TypeId::of::<InsertQuery<E>>(),
        || {
            let mut part_names = String::new();
            let mut placeholders = String::new();

            E::accept_part_visitor(&mut PartNameVisitor(
                &mut part_names,
                &mut placeholders,
                Default::default(),
            ));

            Ok(format!(
                "INSERT INTO `{}` ({}) VALUES ({}) RETURNING `id`",
                E::entity_name(),
                part_names,
                placeholders
            ))
        },
        |mut ctx| {
            value.accept_part_visitor_ref(&mut PartBinder(&mut ctx, 1, Default::default()))?;

            ctx.run()?
                .ok_or(Error::InternalError("No result row from INSERT query"))
                .map(|r| <E::ID>::from_raw(r.read(0).expect("couldn't read resulting ID")))
        },
    )
}

pub(crate) fn insert_ref<E: Entity>(conn: &mut Transaction, value: E::ERef<'_>) -> DBResult<E::ID> {
    struct InsertRefQuery<E: Entity>(std::marker::PhantomData<E>);

    conn.lease().with_prepared(
        std::any::TypeId::of::<InsertRefQuery<E>>(),
        || {
            let mut part_names = String::new();
            let mut placeholders = String::new();

            E::accept_part_visitor(&mut PartNameVisitor(
                &mut part_names,
                &mut placeholders,
                Default::default(),
            ));

            Ok(format!(
                "INSERT INTO `{}` ({}) VALUES ({}) RETURNING `id`",
                E::entity_name(),
                part_names,
                placeholders
            ))
        },
        |mut ctx| {
            let blist = value.as_borrowed_list();
            blist.accept(&mut PartBinder::<E>(&mut ctx, 1, Default::default()))?;

            ctx.run()?
                .ok_or(Error::InternalError("No result row from INSERT query"))
                .map(|r| <E::ID>::from_raw(r.read(0).expect("couldn't read resulting ID")))
        },
    )
}

pub(crate) fn insert_and_return<E: Entity>(
    lease: &mut Transaction,
    mut value: E,
) -> DBResult<Stored<E>> {
    let id = insert(lease, &value)?;

    // update relation data in all fields
    struct DatumWalker<E: Entity>(i64, std::marker::PhantomData<E>);
    impl<E: Entity> EntityPartVisitor for DatumWalker<E> {
        type Entity = E;
        fn visit_datum_mut<EP: EntityPart>(&mut self, datum: &mut EP::Datum) {
            datum.update_adata(RelationData {
                part_name: EP::part_name(),
                local_name: <EP::Entity as Entity>::entity_name(),
                local_id: self.0,
            });
        }
    }

    value.accept_part_visitor_mut(&mut DatumWalker(id.into_raw(), Default::default()));

    Ok(Stored::new(id, value))
}

pub(crate) fn update_entity<E: Entity>(txn: &mut Transaction, value: &Stored<E>) -> DBResult<()> {
    struct UpdateQuery<E: Entity>(std::marker::PhantomData<E>);

    txn.lease().with_prepared(
        std::any::TypeId::of::<UpdateQuery<E>>(),
        || {
            let mut set_columns = String::new();
            struct PartNameVisitor<'a, E: Entity>(&'a mut String, std::marker::PhantomData<E>);
            impl<E: Entity> EntityPartVisitor for PartNameVisitor<'_, E> {
                type Entity = E;
                fn visit<EP: EntityPart>(&mut self) {
                    // only care about the columns
                    if !EP::Datum::INTERNAL_DATUM {
                        return;
                    }

                    if !self.0.is_empty() {
                        self.0.push_str(", ");
                    }
                    self.0.push('`');
                    self.0.push_str(EP::part_name());
                    self.0.push_str("` = ?");
                }
            }

            E::accept_part_visitor(&mut PartNameVisitor(&mut set_columns, Default::default()));
            Ok(format!(
                "UPDATE `{entity_name}` SET {set_columns} WHERE `id` = ?",
                entity_name = E::entity_name()
            ))
        },
        |mut ctx| {
            struct PartBinder<'a, 'b, E: Entity>(
                &'a mut StatementContext<'b>,
                &'a mut i32,
                std::marker::PhantomData<E>,
            );
            impl<E: Entity> EntityPartVisitor for PartBinder<'_, '_, E> {
                type Entity = E;
                fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) -> DBResult<()> {
                    // if the datum has a column, bind it.
                    if EP::Datum::INTERNAL_DATUM {
                        datum.bind_to(self.0, *self.1)?;
                        *self.1 += 1;
                    }
                    Ok(())
                }
            }

            // first bind all the updating clauses
            let mut index = 1;
            value.accept_part_visitor_ref(&mut PartBinder(
                &mut ctx,
                &mut index,
                Default::default(),
            ))?;

            // then bind the id
            value.id().bind_to(&mut ctx, index)?;

            ctx.run()?;

            Ok(())
        },
    )
}

pub(crate) fn do_connect<Remote: Entity>(
    txn: &mut Transaction,
    rdata: &RelationData,
    an: RelationNames,
    remote_id: Remote::ID,
) -> DBResult<()> {
    txn.lease().with_prepared(
        super::hash_of(("connect", an.local_name, an.remote_name, an.part_name)),
        || {
            Ok(format!(
                "insert into `{relation_name}` (`{local_field}`, `{remote_field}`) values (?, ?) returning (`id`)",
                relation_name = an.relation_name(),
                local_field = an.local_field,
                remote_field = an.remote_field
            ))
        },
        |ctx| {
            ctx.bind(1, rdata.local_id)?;
            ctx.bind(2, remote_id.into_raw())?;

            ctx.run()?
                .ok_or(Error::ConstraintViolation("Relation entry uniqueness".to_string()))
                .map(|_| ())
        },
    )
}