Skip to content

implement LoadQuery for batch insert statements for sqlite connection#4616

Merged
weiznich merged 18 commits intodiesel-rs:masterfrom
bwqr:impl-load-query-for-sqlite-batch-insert
Jun 1, 2025
Merged

implement LoadQuery for batch insert statements for sqlite connection#4616
weiznich merged 18 commits intodiesel-rs:masterfrom
bwqr:impl-load-query-for-sqlite-batch-insert

Conversation

@bwqr
Copy link
Contributor

@bwqr bwqr commented May 25, 2025

This is a draft PR requesting for comments. It allows running insert statements with returning clause for sqlite connection. There are a few remaining tasks, which needs a little bit guidance, to consider this PR as ready:

  • Find an appropriate Iterator implementation to return from internal_load (instead of currently used boxed iteration).
  • Implement LoadQuery for Yes case which requires running multiple insert statements. I can implement this if No case looks good.
  • Consider adding tests or examples.
  • Consider using a generic connection with backend Sqlite (similar to ExecuteDsl implementation). I tried to do this but it caused conflicting implementation error. It looks like trait bounds resolution is being improved with newer version of Rust compiler. In the future, it might be possible to use a generic connection bounded by Sqlite backend. For now, we can continue using directly SqliteConnection.
  • Bump MSRV to 1.84 because of compilation errors caused by conflicting implementation error. Check this and this messages for more information

With this PR, following example should compile successfully:

use diesel::prelude::{ExpressionMethods, Queryable};
use diesel::{Connection, RunQueryDsl, SqliteConnection};

diesel::table! {
    apps (id) {
        id -> Int4,
        #[max_length = 8]
        name -> Varchar,
        #[max_length = 8]
        state -> Varchar,
    }
}

#[derive(Queryable)]
struct App {
    id: i32,
    name: String,
    state: String,
}

fn main() {
    let mut conn = SqliteConnection::establish(":memory:").unwrap();

    let query = diesel::insert_into(apps::table).values(vec![
        (apps::name.eq("Good"), apps::state.eq("Running")),
        (apps::name.eq("Bad"), apps::state.eq("Failed")),
    ]);

    query.get_results::<App>(&mut conn);
}

bwqr added 2 commits May 25, 2025 13:19
…qliteConnection`

This implementation allows running insert statements with returning clause.
@weiznich
Copy link
Member

Thanks for opening this PR. There seems to be still a failing CI job here: https://github.com/diesel-rs/diesel/actions/runs/15237082999/job/42852408104?pr=4616. I did not look into the details yet, but that's something that needs to be addressed.

Find an appropriate Iterator implementation to return from internal_load (instead of currently used boxed iteration).

I would suggest to return a "private" wrapper type that implements Iterator, that would enable us to change the details later on as users cannot do anything other than iterating with that type. Essentially what we do here, which puts the type in a private module so that users cannot name it in their code. Ideally that wouldn't use a boxed iterator as inner type, but if that's not easily possible that's not that bad. We can change it later with that setup if it turns out to be problematic.

Consider adding tests or examples.

That would be required.

@bwqr
Copy link
Contributor Author

bwqr commented May 28, 2025

Thank you for your feedback.

There seems to be still a failing CI job here: https://github.com/diesel-rs/diesel/actions/runs/15237082999/job/42852408104?pr=4616. I did not look into the details yet, but that's something that needs to be addressed.

For some reason, the same code causes conflicting implementation for 'InsertStatement<_, BatchInsert<Vec<insert_statement::ValuesClause<_, _>>, _, _, _>, _>' when it is compiled with rust 1.82 compiler. This error confused me a little bit since other checks pass successfully. I will investigate this by trying to compile the code locally with 1.82.

I would suggest to return a "private" wrapper type that implements Iterator, that would enable us to change the details later on as users cannot do anything other than iterating with that type. Essentially what we do here, which puts the type in a private module so that users cannot name it in their code.

I was hoping to use an already defined iterator instead of defining a new one, such as the LoadIter you have linked. However, since LoadIter is private to its module, I am not able to use it inside the insert_with_default_for_sqlite.rs. Should I define a new iterator similar to LoadIter or should we export LoadIter with pub(crate) to prevent duplication?

I will try to write some tests. Until completing all the tasks, I think converting this PR to draft might be the best.

@bwqr bwqr marked this pull request as draft May 28, 2025 10:04
@weiznich
Copy link
Member

weiznich commented May 28, 2025

For some reason, the same code causes conflicting implementation for 'InsertStatement<, BatchInsert<Vec<insert_statement::ValuesClause<, _>>, _, _, _>, _>' when it is compiled with rust 1.82 compiler. This error confused me a little bit since other checks pass successfully. I will investigate this by trying to compile the code locally with 1.82.

I'm not sure if it's related to 1.82. It might also be due to building with --features "sqlite mysql postgres extras", which is not done by the other CI builds. If its related to the rust version we can just bump the MSRV to something that accepts this, this would be fine.

I was hoping to use an already defined iterator instead of defining a new one, such as the LoadIter you have linked. However, since LoadIter is private to its module, I am not able to use it inside the insert_with_default_for_sqlite.rs. Should I define a new iterator similar to LoadIter or should we export LoadIter with pub(crate) to prevent duplication?

If LoadIter is fitting it would be good to use it. It's no problem to make it pub(crate), public. We just don't want to have it in our public API.

@bwqr
Copy link
Contributor Author

bwqr commented May 28, 2025

I'm not sure if it's related to 1.82. It might also be due to building with --features "sqlite mysql postgres extras", which is not done by the other CI builds. If its related to the rust version we can just bump the MSRV to something that accepts this, this would be fine.

I just checked this issue by compiling this PR with a few rust releases with --features "sqlite returning_clauses_for_sqlite_3_35". Here are the results:

  • 1.81: Did not compile due to extern block cannot be declared unsafe used in libsqlite3-sys crate. I guess this was stabilized in 1.82.
  • 1.82: Causes conflicting implementation ❌
  • 1.83: Causes conflicting implementation ❌
  • 1.84: Compiles fine ✅
  • 1.87: Compiles fine ✅

If LoadIter is fitting it would be good to use it. It's no problem to make it pub(crate), public. We just don't want to have it in our public API.

Nice, I will check whether LoadIter is useful for this implementation.

@weiznich
Copy link
Member

I did a quick search which rustc change did introduce that changed behavior just to make sure that this is not by accident and that pointed to rust-lang/rust#130654, which seems to be reasonable. So I suggest to just bump the minimal supported rust version to 1.84. That needs to be done here, here and here.

@bwqr bwqr marked this pull request as ready for review May 29, 2025 15:08
@bwqr
Copy link
Contributor Author

bwqr commented May 29, 2025

I believe this PR is ready for the review :). I would like to thank you for all your guidance and feedback.

Copy link
Member

@weiznich weiznich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. This looks already really good in terms of implementation.
I added another bunch of mostly minor comments about some details that need to be adjusted. As soon as those are fixed, this should be fine to be merged.

Comment on lines 469 to 471
.collect::<Vec<_>>();

Ok(Box::new(results.into_iter()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to skip the collect here and return the iterator directly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to mention the new feature in the Changelog 😉

.next()
.ok_or(crate::result::Error::NotFound)?;

results.push(result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also want to unwrap the inner result to stop the transaction from being comited in the error test. We likely want to have a test case for this (Just insert a reasonable item + a duplicated item and make sure that none are in the database)

Suggested change
results.push(result);
results.push(Ok(result?));

(If we would like to be even more correct: We should roll back the transaction for anything but deserialization errors to perfectly mirror the batch variant)

There is a test for batch inserts without returning here:

fn batch_insert_is_atomic_on_sqlite() {
You likely can just copy and adjust that test for insert + returning and upserts.

Otherwise it sounds like a good idea to quickly go over the tests cfg out for sqlite in that file and check which ones can now be enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing this out. I updated code to bubble up errors earlier other than deserialization one as you said. And added a few case to test this. Please do not hesitate to suggest any other improvement for both code and tests (current implementation does a match on the result but there might be a better way to do it).

.next()
.ok_or(crate::result::Error::NotFound)?;

results.push(result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

};

Ok(crate::query_dsl::load_dsl::LoadIter {
cursor: <_ as crate::connection::LoadConnection<B>>::load(conn, query)?,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, that's really nice 🎉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed 😃 , but I found another way to run the query, which eliminated using LoadIter explicitly. I reverted changes related to making LoadIter pub(crate).

Copy link
Member

@weiznich weiznich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now, thanks for updating.

(I just pushed a changelog entry, otherwise this is now ready to be merged)

@weiznich weiznich enabled auto-merge June 1, 2025 15:56
@weiznich weiznich added this pull request to the merge queue Jun 1, 2025
Merged via the queue into diesel-rs:master with commit 900bed9 Jun 1, 2025
34 checks passed
@weiznich weiznich mentioned this pull request Sep 5, 2025
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments