Skip to content

Feature Request: Add a QueryBuilder method to push value tuples without VALUES #4378

Description

@guptasomnath

QueryBuilder::push_values() automatically adds the VALUES keyword every time it is called.

This works well for a single call, but becomes problematic when push_values() needs to be called multiple times while constructing one bulk INSERT query, especially when the data comes from nested collections.

I would like to propose adding a way to push generated value tuples without automatically adding the VALUES keyword.

Description

Consider the following use case:

for (index, varient) in varients.iter().enumerate() {
    if let Some(product_varient) = product.product_varient.get(index) {
        qb2.push_values(&product_varient.varient_images, |mut b, single_varient| {
            b.push_bind(&single_varient.image_url)
                .push_bind(single_varient.display_order)
                .push_bind(varient.id);
        });
    }
}

When push_values() is called multiple times, each invocation adds the VALUES keyword.

For example, the generated SQL can become:

VALUES ($1, $2, $3), ($4, $5, $6)
VALUES ($7, $8, $9), ($10, $11, $12)

This is invalid SQL.

The desired output is:

VALUES
    ($1, $2, $3),
    ($4, $5, $6),
    ($7, $8, $9),
    ($10, $11, $12)

The underlying problem is that push_values() currently combines two responsibilities:

  1. Adding the VALUES keyword.
  2. Generating comma-separated parenthesized value tuples.

When building queries dynamically, these two operations sometimes need to be controlled independently.

Prefered solution

Add a push_args() method that performs the tuple-generation part of push_values() without adding the VALUES keyword.

For example:

pub fn push_args<I, F>(&mut self, tuples: I, mut push_tuple: F) -> &mut Self
where
    I: IntoIterator,
    F: FnMut(Separated<'_, 'args, DB, &'static str>, I::Item),
{
    self.sanity_check();

    let mut separated = self.separated(", ");

    for tuple in tuples {
        separated.push("(");

        push_tuple(separated.query_builder.separated(", "), tuple);

        separated.push_unseparated(")");
    }

    debug_assert!(
        separated.push_separator,
        "No value being pushed. QueryBuilder may not build correct sql query!"
    );

    separated.query_builder
}

Then the caller can explicitly control the VALUES keyword:

let mut qb = QueryBuilder::new(
    "INSERT INTO product_variant_images \
     (image_url, display_order, variant_id) VALUES "
);

let mut first = true;

for (index, varient) in varients.iter().enumerate() {
    if let Some(product_varient) = product.product_varient.get(index) {
        if !first {
            qb.push(", ");
        }

        first = false;

        qb.push_args(&product_varient.varient_images, |mut b, single_varient| {
            b.push_bind(&single_varient.image_url)
                .push_bind(single_varient.display_order)
                .push_bind(varient.id);
        });
    }
}

This produces a single VALUES clause with all tuples.

The existing push_values() API can remain unchanged. Its implementation could potentially reuse push_args() internally:

pub fn push_values<I, F>(&mut self, tuples: I, push_tuple: F) -> &mut Self
where
    I: IntoIterator,
    F: FnMut(Separated<'_, 'args, DB, &'static str>, I::Item),
{
    self.push("VALUES ");
    self.push_args(tuples, push_tuple)
}

I'm open to feedback on the method name or API design if there is a preferred approach for QueryBuilder.

If the maintainers agree that this API/use case is appropriate, I would be happy to implement this change and submit the PR, including the necessary tests.

If possible, could you please confirm whether I can take this issue up for implementation? I would like to work on the implementation and tests myself rather than having multiple contributors work on the same change.

I'm also happy to adjust the proposed API or implementation based on the maintainers' feedback before starting the PR.

Is this a breaking change? Why or why not?

No, this should not be a breaking change.

The proposed push_args() method is a new API and does not change the existing behavior or signature of push_values().

Existing code using push_values() will continue to work as before.

If push_values() is internally refactored to use push_args(), the externally observable behavior of push_values() remains unchanged.

  1. SQLX VERSION : sqlx = { version = "0.8" }
  2. DATABASE DRIVER : Postgresql
  3. DATABASE VERSION : 17

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions