Skip to content

Commit 06fff19

Browse files
committed
fix(query-builders): never skip writes when partitioning
The partitioning logic had a bug: if any write query contained more parameters than than `max_bind_values`, the entire write was skipped. It's rather unlikely for this to happen in practice, but it was easily possible in our tests, because we set `QUERY_BATCH_SIZE` to 10 in tests in order to test partitioning and chunking. This change ensures that we never skip writes when partitioning and put the writes which are too large into their own separate batches (and let them fail on the database level if they genuinely exceed the real parameter limit of the database). An alternative would've been to panic or return an error ourselves (and raise the `QUERY_BATCH_SIZE` in tests and update the tests which depend on it) but this seems preferable to me.
1 parent 610a68f commit 06fff19

3 files changed

Lines changed: 87 additions & 30 deletions

File tree

  • query-engine
    • connector-test-kit-rs
      • query-engine-tests/tests/writes/top_level_mutations
      • query-tests-setup/src/connector_tag
    • query-builders/sql-query-builder/src

query-engine/connector-test-kit-rs/query-engine-tests/tests/writes/top_level_mutations/create_many.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,58 @@ mod create_many {
473473

474474
Ok(())
475475
}
476+
477+
fn schema_8() -> String {
478+
indoc! {
479+
r#"
480+
model TestModel {
481+
#id(id, Int, @id)
482+
field1 Int
483+
field2 Int
484+
field3 Int
485+
field4 Int
486+
field5 Int
487+
field6 Int
488+
field7 Int
489+
field8 Int
490+
field9 Int
491+
field10 Int
492+
field11 Int
493+
field12 Int
494+
}
495+
"#
496+
}
497+
.to_owned()
498+
}
499+
500+
#[connector_test(schema(schema_8))]
501+
async fn create_many_with_many_fields(runner: Runner) -> TestResult<()> {
502+
const FIELDS_IN_TEST_MODEL: usize = 12;
503+
504+
if let Some(max_bind_values) = runner.max_bind_values() {
505+
if !runner.connector_version().is_wasm() {
506+
assert!(max_bind_values < FIELDS_IN_TEST_MODEL, "When QUERY_BATCH_SIZE is set, its value must be less than {FIELDS_IN_TEST_MODEL}, otherwise the test will not be testing what it's supposed to; instead got {max_bind_values}. Either update this test, or the QUERY_BATCH_SIZE env var value in .envrc and GitHub Actions pipelines");
507+
}
508+
}
509+
510+
let result = run_query!(
511+
runner,
512+
r#"mutation {
513+
createManyTestModel(data: [
514+
{ id: 1, field1: 1, field2: 2, field3: 3, field4: 4, field5: 5, field6: 6, field7: 7, field8: 8, field9: 9, field10: 10, field11: 11, field12: 12 },
515+
{ id: 2, field1: 1, field2: 2, field3: 3, field4: 4, field5: 5, field6: 6, field7: 7, field8: 8, field9: 9, field10: 10, field11: 11, field12: 12 },
516+
{ id: 3, field1: 1, field2: 2, field3: 3, field4: 4, field5: 5, field6: 6, field7: 7, field8: 8, field9: 9, field10: 10, field11: 11, field12: 12 },
517+
{ id: 4, field1: 1, field2: 2, field3: 3, field4: 4, field5: 5, field6: 6, field7: 7, field8: 8, field9: 9, field10: 10, field11: 11, field12: 12 },
518+
]) {
519+
count
520+
}
521+
}"#
522+
);
523+
524+
insta::assert_snapshot!(result, @r#"{"data":{"createManyTestModel":{"count":4}}}"#);
525+
526+
Ok(())
527+
}
476528
}
477529

478530
#[test_suite(schema(json_opt), exclude(MySql(5.6)), capabilities(CreateMany, Json))]

query-engine/connector-test-kit-rs/query-tests-setup/src/connector_tag/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ impl ConnectorVersion {
319319
/// Determines if the connector uses a driver adapter implemented in Wasm.
320320
/// Do not delete! This is used because the `#[cfg(target_arch = "wasm32")]` conditional compilation
321321
/// directive doesn't work in the test runner.
322-
fn is_wasm(&self) -> bool {
322+
pub fn is_wasm(&self) -> bool {
323323
matches!(
324324
self,
325325
Self::Postgres(Some(PostgresVersion::PgJsWasm))

query-engine/query-builders/sql-query-builder/src/write.rs

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -410,43 +410,48 @@ fn collect_affected_fields(args: &[WriteArgs], model: &Model) -> HashSet<ScalarF
410410

411411
/// Partitions data into batches, respecting `max_bind_values` and `max_insert_rows` settings from
412412
/// the `Context`.
413+
///
414+
/// We need to split inserts if they are above a parameter threshold, as well as split based on number of rows:
415+
/// horizontal partitioning by row number, vertical by number of args.
413416
fn partition_into_batches(args: Vec<WriteArgs>, ctx: &Context<'_>) -> Vec<Vec<WriteArgs>> {
414417
let batches = if let Some(max_params) = ctx.max_bind_values() {
415-
// We need to split inserts if they are above a parameter threshold, as well as split based on number of rows.
416-
// -> Horizontal partitioning by row number, vertical by number of args.
417-
args.into_iter()
418-
.peekable()
419-
.batching(|iter| {
420-
let mut param_count: usize = 0;
421-
let mut batch = vec![];
422-
423-
while param_count < max_params {
424-
// If the param count _including_ the next item doens't exceed the limit,
425-
// we continue filling up the current batch.
426-
let proceed = match iter.peek() {
427-
Some(next) => (param_count + next.len()) <= max_params,
428-
None => break,
429-
};
418+
#[derive(Default)]
419+
struct Batch {
420+
items: Vec<WriteArgs>,
421+
param_count: usize,
422+
}
423+
424+
impl Batch {
425+
fn add(&mut self, item: WriteArgs) {
426+
let len = item.len();
427+
self.items.push(item);
428+
self.param_count += len;
429+
}
430+
}
430431

431-
if proceed {
432-
match iter.next() {
433-
Some(next) => {
434-
param_count += next.len();
435-
batch.push(next)
436-
}
437-
None => break,
438-
}
432+
impl From<WriteArgs> for Batch {
433+
fn from(args: WriteArgs) -> Self {
434+
let mut batch = Self::default();
435+
batch.add(args);
436+
batch
437+
}
438+
}
439+
440+
args.into_iter()
441+
.fold(Vec::<Batch>::new(), |mut acc, item| {
442+
if let Some(last_batch) = acc.last_mut() {
443+
if last_batch.param_count + item.len() > max_params {
444+
acc.push(item.into());
439445
} else {
440-
break;
446+
last_batch.add(item);
441447
}
442-
}
443-
444-
if batch.is_empty() {
445-
None
446448
} else {
447-
Some(batch)
449+
acc.push(item.into());
448450
}
451+
acc
449452
})
453+
.into_iter()
454+
.map(|batch| batch.items)
450455
.collect_vec()
451456
} else {
452457
vec![args]

0 commit comments

Comments
 (0)