Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Entity manipulation CLI

One optional feature that microrm supports is autogeneration of a CLI for inspecting and manipulating database entities, enabled by the cli feature.

use microrm::prelude::*;

// start by defining a simple schema
struct PetOwners;
impl microrm::Relation for PetOwners {
    type Domain = Owner;
    type Codomain = Pet;
    const NAME: &'static str = "PetOwners";
    const UNIQUE_CODOMAIN: bool = true;
}
#[derive(Entity)]
struct Pet {
    #[key]
    name: String,
    species: String,
    owner: microrm::Codomain<PetOwners>,
}
#[derive(Entity)]
struct Owner {
    #[key]
    name: String,
    address: String,
    pets: microrm::Domain<PetOwners>,
}
#[derive(Schema)]
struct Schema {
    owners: microrm::Table<Owner>,
    pets: microrm::Table<Pet>,
}

// make a very simple clap parser via the derive interface:
#[derive(clap::Parser)]
struct Invocation {
    #[clap(subcommand)]
    cmd: InvocationSubcommand,
}
#[derive(clap::Subcommand)]
enum InvocationSubcommand {
    Pet {
        #[clap(subcommand)]
        cmd: microrm::cli::Autogenerate<PetInterface>,
    },
    Owner {
        #[clap(subcommand)]
        cmd: microrm::cli::Autogenerate<OwnerInterface>,
    },
}

// now we need to define two helper types: PetInterface and OwnerInterface. these tell the CLI
// generation how to deal with these two entities.
struct PetInterface;
impl microrm::cli::EntityInterface for PetInterface {
    type Entity = Pet;
    type Error = microrm::Error;
    // no custom context needed
    type Context = ();
    // by default, microrm does not generate a "new" or "create" command, so we need to add it
    // here.
    type CustomCommand = PetCustom;

    // and a handler for it
    fn run_custom(
        _ctx: &Self::Context,
        cmd: Self::CustomCommand,
        txn: &mut microrm::Transaction,
        query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
    ) -> Result<(), Self::Error> {
        match cmd {
            PetCustom::Create { name, species } => {
                query_ctx.insert(
                    txn,
                    Pet {
                        name,
                        species,
                        owner: Default::default(),
                    },
                )?;
            },
        }
        Ok(())
    }
}
#[derive(Debug, clap::Subcommand)]
enum PetCustom {
    Create { name: String, species: String },
}

struct OwnerInterface;
impl microrm::cli::EntityInterface for OwnerInterface {
    type Entity = Owner;
    type Error = microrm::Error;
    // no custom context needed
    type Context = ();
    // as before, add a 'create' command
    type CustomCommand = OwnerCustom;

    // and again, a handler for it...
    fn run_custom(
        _ctx: &Self::Context,
        cmd: Self::CustomCommand,
        txn: &mut microrm::Transaction,
        query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
    ) -> Result<(), Self::Error> {
        match cmd {
            OwnerCustom::Create { name, address } => {
                query_ctx.insert(
                    txn,
                    Owner {
                        name,
                        address,
                        pets: Default::default(),
                    },
                )?;
            },
        }
        Ok(())
    }
}
#[derive(Debug, clap::Subcommand)]
enum OwnerCustom {
    Create { name: String, address: String },
}

fn main() -> microrm::DBResult<()> {
    use clap::Parser;
    let inv = Invocation::parse();
    // open the database
    let (pool, schema) = microrm::ConnectionPool::open::<Schema>("simple_cli.db")?;
    let mut txn = pool.start()?;
    match inv.cmd {
        InvocationSubcommand::Pet { cmd } => cmd.perform(&(), &mut txn, &schema.pets)?,
        InvocationSubcommand::Owner { cmd } => cmd.perform(&(), &mut txn, &schema.owners)?,
    }
    txn.commit()?;

    Ok(())
}

/* annotated interaction transcript:
    $ alias simple_cli='cargo run -qF cli --example simple_cli'
Create some pets, who must have unique names:
    $ simple_cli pet create louie cat
    $ simple_cli pet create louie dog
    Error: ConstraintViolation("UNIQUE constraint failed: pet.name")
    $ simple_cli pet create archibald dog
Create an owner:
    $ simple_cli owner create alison "123 Some Street"
Attach louie to alison:
    $ simple_cli owner attach alison pets louie
    $ simple_cli owner inspect alison
    Owner {
        name: "alison",
        address: "123 Some Street",
    }
    pets: (1)
    [#  1]: Pet { name: "louie", species: "cat" }
Verify that the connection goes both ways:
    $ simple_cli pet inspect louie
    Pet {
        name: "louie",
        species: "cat",
    }
    owner: (1)
    [#  1]: Owner { name: "alison", address: "123 Some Street" }
Add a second owner and try to give them louie as well:
    $ simple_cli owner create barbara "234 Some Street"
    $ simple_cli owner attach barbara pets louie
    Error: ConstraintViolation("UNIQUE constraint failed: owner_pet_relation_PetOwners.range")
But we can, of course, give barbara a different pet:
    $ simple_cli owner attach barbara pets archibald
*/