diff --git a/libs/prisma-models/src/field.rs b/libs/prisma-models/src/field.rs index 0d3b2712fc72..f0994f36573c 100644 --- a/libs/prisma-models/src/field.rs +++ b/libs/prisma-models/src/field.rs @@ -58,6 +58,10 @@ impl DataSourceField { pub fn model_field(&self) -> Field { self.model_field.upgrade() } + + pub fn name(&self) -> &str { + self.backing_field.name.as_str() + } } impl Deref for DataSourceField { diff --git a/libs/prisma-models/src/field/relation.rs b/libs/prisma-models/src/field/relation.rs index ebab317156a4..738b532be330 100644 --- a/libs/prisma-models/src/field/relation.rs +++ b/libs/prisma-models/src/field/relation.rs @@ -173,9 +173,9 @@ impl RelationField { } pub fn model(&self) -> ModelRef { - self.model - .upgrade() - .expect("Model does not exist anymore. Parent model got deleted without deleting the child.") + self.model.upgrade().expect( + "Model does not exist anymore. Parent model got deleted without deleting the child.", + ) } pub fn relation(&self) -> RelationRef { @@ -250,6 +250,8 @@ impl RelationField { } pub fn db_names(&self) -> impl Iterator { - self.data_source_fields().into_iter().map(|dsf| dsf.name.as_str()) + self.data_source_fields() + .into_iter() + .map(|dsf| dsf.name.as_str()) } } diff --git a/libs/prisma-models/src/record.rs b/libs/prisma-models/src/record.rs index 3b385f6b1b80..b71ccfc63a64 100644 --- a/libs/prisma-models/src/record.rs +++ b/libs/prisma-models/src/record.rs @@ -1,4 +1,7 @@ -use crate::{DataSourceFieldRef, DomainError, ModelProjection, PrismaValue, RecordProjection}; +use crate::{ + DataSourceFieldRef, DomainError, Field, ModelProjection, OrderBy, PrismaValue, RecordProjection, +}; +use std::collections::HashMap; #[derive(Debug, Clone)] pub struct SingleRecord { @@ -17,7 +20,10 @@ impl Into for SingleRecord { impl SingleRecord { pub fn new(record: Record, field_names: Vec) -> Self { - Self { record, field_names } + Self { + record, + field_names, + } } pub fn projection(&self, projection: &ModelProjection) -> crate::Result { @@ -36,7 +42,59 @@ pub struct ManyRecords { } impl ManyRecords { - pub fn projections(&self, model_projection: &ModelProjection) -> crate::Result> { + pub fn new(field_names: Vec) -> Self { + Self { + records: Vec::new(), + field_names, + } + } + + pub fn order_by(&mut self, order_by: &OrderBy) { + let field_indices: HashMap<&str, usize> = self + .field_names + .iter() + .enumerate() + .map(|(i, name)| (name.as_str(), i)) + .collect(); + + self.records.sort_by(|a, b| match order_by.field { + Field::Scalar(ref sf) => { + let index = field_indices[sf.db_name()]; + + if order_by.sort_order.is_ascending() { + a.values[index].cmp(&b.values[index]) + } else { + b.values[index].cmp(&a.values[index]) + } + } + Field::Relation(ref rf) => { + let ds_fields = rf.data_source_fields(); + let mut a_vals = Vec::with_capacity(ds_fields.len()); + let mut b_vals = Vec::with_capacity(ds_fields.len()); + + for dsf in ds_fields { + let index = field_indices[dsf.name()]; + a_vals.push(&a.values[index]); + b_vals.push(&b.values[index]); + } + + if order_by.sort_order.is_ascending() { + a_vals.cmp(&b_vals) + } else { + b_vals.cmp(&a_vals) + } + } + }) + } + + pub fn push(&mut self, record: Record) { + self.records.push(record); + } + + pub fn projections( + &self, + model_projection: &ModelProjection, + ) -> crate::Result> { self.records .iter() .map(|record| { @@ -123,16 +181,24 @@ impl Record { Ok(x) } - pub fn get_field_value(&self, field_names: &[String], field: &str) -> crate::Result<&PrismaValue> { - let index = field_names.iter().position(|r| r == field).map(Ok).unwrap_or_else(|| { - Err(DomainError::FieldNotFound { - name: field.to_string(), - model: format!( - "Field not found in record {:?}. Field names are: {:?}, looking for: {:?}", - &self, &field_names, field - ), - }) - })?; + pub fn get_field_value( + &self, + field_names: &[String], + field: &str, + ) -> crate::Result<&PrismaValue> { + let index = field_names + .iter() + .position(|r| r == field) + .map(Ok) + .unwrap_or_else(|| { + Err(DomainError::FieldNotFound { + name: field.to_string(), + model: format!( + "Field not found in record {:?}. Field names are: {:?}, looking for: {:?}", + &self, &field_names, field + ), + }) + })?; Ok(&self.values[index]) } diff --git a/query-engine/connector-test-kit/src/test/scala/queries/batch/InSelectionBatching.scala b/query-engine/connector-test-kit/src/test/scala/queries/batch/InSelectionBatching.scala index f31a8c398d61..57dd3f45b8e2 100644 --- a/query-engine/connector-test-kit/src/test/scala/queries/batch/InSelectionBatching.scala +++ b/query-engine/connector-test-kit/src/test/scala/queries/batch/InSelectionBatching.scala @@ -5,19 +5,18 @@ import util.{ApiSpecBase, ProjectDsl} class InSelectionBatching extends FlatSpec with Matchers with ApiSpecBase { val project = ProjectDsl.fromString { - """model Artist { - | id String @id @default(cuid()) - | ArtistId Int @unique - | Name String - | Albums Album[] + """model A { + | id Int @id + | b B + | c C |} - | - |model Album { - | id String @id @default(cuid()) - | AlbumId Int @unique - | Title String - | Artist Artist @relation(references: [id]) - | @@index([Artist]) + |model B { + | id Int @id + | as A[] + |} + |model C { + | id Int @id + | as A[] |} |""" } @@ -27,42 +26,47 @@ class InSelectionBatching extends FlatSpec with Matchers with ApiSpecBase { database.setup(project) server.query( - """mutation artistWithoutAlbums {createArtist(data:{ - | Name: "ArtistWithoutAlbums" - | ArtistId: 1 - |}){Name}}""", + """mutation a {createA(data:{ + | id: 1 + | b: { create: { id: 1 } } + | c: { create: { id: 1 } } + |}){id}}""", project = project ) server.query( - """mutation artistWithAlbumButWithoutTracks {createArtist(data:{ - | Name: "ArtistWithOneAlbumWithoutTracks" - | ArtistId: 2, - |}){Name}}""", + """mutation a {createA(data:{ + | id: 2 + | b: { connect: { id: 1 } } + | c: { create: { id: 2 } } + |}){id}}""", project = project ) server.query( - """mutation artistWithAlbumButWithoutTracks {createArtist(data:{ - | Name: "Three" - | ArtistId: 3, - |}){Name}}""", + """mutation a {createA(data:{ + | id: 3 + | b: { create: { id: 3 } } + | c: { create: { id: 3 } } + |}){id}}""", project = project ) server.query( - """mutation artistWithAlbumButWithoutTracks {createArtist(data:{ - | Name: "Four" - | ArtistId: 4, - |}){Name}}""", + """mutation a {createA(data:{ + | id: 4 + | b: { create: { id: 4 } } + | c: { create: { id: 4 } } + |}){id}}""", project = project ) server.query( - """mutation artistWithAlbumButWithoutTracks {createArtist(data:{ - | Name: "Five" - | ArtistId: 5, - |}){Name}}""", + """mutation a {createA(data:{ + | id: 5 + | b: { create: { id: 5 } } + | c: { create: { id: 5 } } + |}){id}}""", project = project ) } @@ -70,7 +74,7 @@ class InSelectionBatching extends FlatSpec with Matchers with ApiSpecBase { "batching of IN queries" should "work when having more than the specified amount of items" in { val res = server.query( """query idInTest { - | findManyArtist(where: { ArtistId_in: [5,4,3,2,1,1,1,2,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }) { ArtistId } + | findManyA(where: { id_in: [5,4,3,2,1,1,1,2,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }) { id } |} |""".stripMargin, project = project, @@ -79,14 +83,14 @@ class InSelectionBatching extends FlatSpec with Matchers with ApiSpecBase { ) res.toString should be( - """{"data":{"findManyArtist":[{"ArtistId":1},{"ArtistId":2},{"ArtistId":3},{"ArtistId":4},{"ArtistId":5}]}}""".stripMargin + """{"data":{"findManyA":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}]}}""".stripMargin ) } "ascending ordering of batched IN queries" should "work when having more than the specified amount of items" in { val res = server.query( """query idInTest { - | findManyArtist(where: { ArtistId_in: [5,4,3,2,1,2,1,1,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }, orderBy: ArtistId_ASC) { ArtistId } + | findManyA(where: { id_in: [5,4,3,2,1,2,1,1,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }, orderBy: id_ASC) { id } |} |""".stripMargin, project = project, @@ -95,14 +99,56 @@ class InSelectionBatching extends FlatSpec with Matchers with ApiSpecBase { ) res.toString should be( - """{"data":{"findManyArtist":[{"ArtistId":1},{"ArtistId":2},{"ArtistId":3},{"ArtistId":4},{"ArtistId":5}]}}""".stripMargin + """{"data":{"findManyA":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}]}}""".stripMargin ) } "descending ordering of batched IN queries" should "work when having more than the specified amount of items" in { val res = server.query( """query idInTest { - | findManyArtist(where: { ArtistId_in: [5,4,3,2,1,1,1,2,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }, orderBy: ArtistId_DESC) { ArtistId } + | findManyA(where: {id_in: [5,4,3,2,1,1,1,2,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }, orderBy: id_DESC) { id } + |} + |""".stripMargin, + project = project, + legacy = false, + batchSize = 2, + ) + + res.toString should be( + """{"data":{"findManyA":[{"id":5},{"id":4},{"id":3},{"id":2},{"id":1}]}}""".stripMargin + ) + } + + "ascending ordering of batched IN with relation field" should "work" in { + val res = server.query( + """ + |query { + | findManyB { + | as(orderBy: c_ASC) { + | c { id } + | } + | } + |} + |""".stripMargin, + project = project, + legacy = false, + batchSize = 2, + ) + + res.toString should be( + """{"data":{"findManyB":[{"as":[{"c":{"id":1}},{"c":{"id":2}}]},{"as":[{"c":{"id":3}}]},{"as":[{"c":{"id":4}}]},{"as":[{"c":{"id":5}}]}]}}""".stripMargin + ) + } + + "descending ordering of batched IN with relation field" should "work" in { + val res = server.query( + """ + |query { + | findManyB { + | as(orderBy: c_DESC) { + | c { id } + | } + | } |} |""".stripMargin, project = project, @@ -111,7 +157,7 @@ class InSelectionBatching extends FlatSpec with Matchers with ApiSpecBase { ) res.toString should be( - """{"data":{"findManyArtist":[{"ArtistId":5},{"ArtistId":4},{"ArtistId":3},{"ArtistId":2},{"ArtistId":1}]}}""".stripMargin + """{"data":{"findManyB":[{"as":[{"c":{"id":2}},{"c":{"id":1}}]},{"as":[{"c":{"id":3}}]},{"as":[{"c":{"id":4}}]},{"as":[{"c":{"id":5}}]}]}}""".stripMargin ) } } diff --git a/query-engine/connectors/sql-query-connector/src/database/operations/read.rs b/query-engine/connectors/sql-query-connector/src/database/operations/read.rs index 361f57f08763..c74558dac11a 100644 --- a/query-engine/connectors/sql-query-connector/src/database/operations/read.rs +++ b/query-engine/connectors/sql-query-connector/src/database/operations/read.rs @@ -6,7 +6,6 @@ use connector_interface::*; use futures::stream::{FuturesUnordered, StreamExt}; use prisma_models::*; use quaint::ast::*; -use std::collections::HashMap; pub async fn get_single_record( conn: &dyn QueryExt, @@ -39,9 +38,9 @@ pub async fn get_many_records( mut query_arguments: QueryArguments, selected_fields: &SelectedFields, ) -> crate::Result { - let field_names: Vec = selected_fields.db_names().map(String::from).collect(); + let field_names = selected_fields.db_names().map(String::from).collect(); let idents: Vec<_> = selected_fields.types().collect(); - let mut records = Vec::new(); + let mut records = ManyRecords::new(field_names); if query_arguments.can_batch() { // We don't need to order in the database due to us ordering in this @@ -62,25 +61,8 @@ pub async fn get_many_records( } } - let field_indices: HashMap<&str, usize> = field_names - .iter() - .enumerate() - .map(|(i, name)| (name.as_str(), i)) - .collect(); - - if let Some(order_by) = order { - records.sort_by(|a, b| match &order_by.field { - Field::Scalar(sf) => { - let index = field_indices[sf.db_name()]; - - if order_by.sort_order.is_ascending() { - a.values[index].cmp(&b.values[index]) - } else { - b.values[index].cmp(&a.values[index]) - } - } - Field::Relation(_) => todo!(), - }) + if let Some(ref order_by) = order { + records.order_by(order_by) } } else { let query = read::get_records(model, selected_fields.columns(), query_arguments); @@ -94,10 +76,7 @@ pub async fn get_many_records( } } - Ok(ManyRecords { - records, - field_names, - }) + Ok(records) } pub async fn get_related_m2m_record_ids(