neoeinstein / modyne

An opinionated Rust library for interacting with AWS DynamoDB single-table designs.
Apache License 2.0
52 stars 1 forks source link

Question: type-safe builder for updates #12

Open happylinks opened 9 months ago

happylinks commented 9 months ago

I was wondering if you've thought about making the updates type-safe somehow. Right now the updates are done like this:

let key = OrderKeyInput {
    user_name,
    order_id,
};

let expression = expr::Update::new("SET #status = :status")
    .name("#status", "status")
    .value(":status", status);

Order::update(key)
    .expression(expression)
    .execute(self)
    .await?;

But what if they could be done like:

let key = OrderKeyInput {
    user_name,
    order_id,
};

Order::update(key)
    .status(status)
    .execute(self)
    .await?;

OR

Order::update(key)
    .expression(Order::updateBuilder().status(status).build())
    .execute(self)
    .await?;

I'm not sure how this would work yet (still relatively new to rust), but from an api perspective this would be amazing. You'd abstract away the dynamodb syntax and prevent bugs due to typos there as well. For more specific expressions you could still use the current approach.

Curious about your opinion!

neoeinstein commented 9 months ago

I would love to see this too. For the first iteration, I didn't have a clear model of how to do it safely, especially with the myriad things that a single update expression can do at a time. If I were to make something, it would probably need to be a procedural macro that could parse out the various elements and then turn that into a structure that you could use.

happylinks commented 9 months ago

I was looking into https://docs.rs/derive_builder/0.12.0/derive_builder/ a bit, seems like it could be relevant. It automatically generates a builder pattern for a struct. We'd want it to do this for a copy of the struct that has optional values for everything I think. Then we could use that struct to create the update expression and pass the names and values. Definitely not a complete picture yet, but wanted to share anyway :)

happylinks commented 9 months ago

Also found this one: https://github.com/yanganto/struct-patch/tree/main

let mut patch = Story::new_empty_patch();
patch.archived = Some(true);
println!("patch: {:#?}", patch);
patch: StoryPatch {
    entity_type: None,
    story_id: None,
    name: None,
    creator: None,
    creator_org_id: None,
    anon_creator_id: None,
    archived: Some(
        true,
    ),
    link_scope: None,
    link_role: None,
    views: None,
    slug: None,
    created_at: None,
    updated_at: None,
    scene_ids: None,
    cta: None,
    captions_default_enabled: None,
    view_count_enabled: None,
    comments_enabled: None,
    dimensions: None,
    ttl: None,
}
happylinks commented 9 months ago

Continuing that patch test a bit:

let mut patch = Story::new_empty_patch();
patch.archived = Some(true);
println!("patch: {:#?}", patch);

let json = serde_json::to_value(&patch).unwrap();
let non_none_keys: Vec<_> = json
    .as_object()
    .unwrap()
    .iter()
    .filter(|(_, v)| !v.is_null())
    .map(|(k, _)| k.to_string())
    .collect();

let update_expression = format!(
    "SET {}",
    non_none_keys
        .iter()
        .map(|key| { format!("#{} = :{}", key, key) })
        .collect::<Vec<String>>()
        .join(", ")
);
println!("update_expression: {:#?}", update_expression);

let mut expression = expr::Update::new(&update_expression);
for key in non_none_keys {
    expression = expression.name(&format!("#{}", key), &key);
    expression = expression.value(&format!(":{}", key), &json[key]);
}

println!("expression: {:#?}", expression);
patch: StoryPatch {
    entity_type: None,
    story_id: None,
    name: None,
    creator: None,
    creator_org_id: None,
    anon_creator_id: None,
    archived: Some(
        true,
    ),
    link_scope: None,
    link_role: None,
    views: None,
    slug: None,
    created_at: None,
    updated_at: None,
    scene_ids: None,
    cta: None,
    captions_default_enabled: None,
    view_count_enabled: None,
    comments_enabled: None,
    dimensions: None,
    ttl: None,
}
update_expression: "SET #archived = :archived"
expression: Update {
    expression: "SET #upd_archived = :upd_archived",
    names: [
        (
            "#upd_archived",
            "archived",
        ),
    ],
    values: [
        (
            ":upd_archived",
            Bool(
                true,
            ),
        ),
    ],
    sensitive_values: <0 values>,
}

Kind of cool! Definitely not ideal serialising it to json to figure out the non-None keys, and taking the value from the json, but maybe this will help us get into the right direction :)

MinisculeGirraffe commented 8 months ago

https://github.com/serde-rs/serde/issues/984#issuecomment-314143738

Throwing this out there as possibly relevant