-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add PgBindIter for encoding and use it as the implementation encoding &[T] #3651
Open
tylerhawkes
wants to merge
7
commits into
launchbadge:main
Choose a base branch
from
tylerhawkes:add-postgres-bind-iter
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+217
−29
Open
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7f01cfb
Add PgBindIter for encoding and use it as the implementation encoding…
tylerhawkes ba7525f
Implement suggestions from review
tylerhawkes afe6b19
Add docs to PgBindIter and test to ensure it works for owned and borr…
tylerhawkes 4028058
Use extension trait for iterators to allow code to flow better. Make …
tylerhawkes 7768e24
Fix doc function
tylerhawkes 9f8e03a
Fix doc test to actually compile
tylerhawkes 6ae3505
Use Cell<Option<I>> instead of Clone bound
tylerhawkes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
use sqlx_core::{ | ||
database::Database, | ||
encode::{Encode, IsNull}, | ||
error::BoxDynError, | ||
types::Type, | ||
}; | ||
|
||
use crate::{type_info::PgType, PgArgumentBuffer, PgHasArrayType, PgTypeInfo, Postgres}; | ||
|
||
// not exported but pub because it is used in the extension trait | ||
pub struct PgBindIter<I>(I); | ||
|
||
/// Iterator extension trait enabling iterators to encode arrays in Postgres. | ||
/// | ||
/// Because of the blanket impl of `PgHasArrayType` for all references | ||
/// we can borrow instead of needing to clone or copy in the iterators | ||
/// and it still works | ||
/// | ||
/// Previously, 3 separate arrays would be needed in this example which | ||
/// requires iterating 3 times to collect items into the array and then | ||
/// iterating over them again to encode. | ||
/// | ||
/// This now requires only iterating over the array once for each field | ||
/// while using less memory giving both speed and memory usage improvements | ||
/// along with allowing much more flexibility in the underlying collection. | ||
/// | ||
/// ```rust,no_run | ||
/// # async fn test_bind_iter() -> Result<(), sqlx::error::BoxDynError> { | ||
/// # use sqlx::types::chrono::{DateTime, Utc}; | ||
/// # use sqlx::Connection; | ||
/// # fn people() -> &'static [Person] { | ||
/// # &[] | ||
/// # } | ||
/// # let mut conn = <sqlx::Postgres as sqlx::Database>::Connection::connect("dummyurl").await?; | ||
/// use sqlx::postgres::PgBindIterExt; | ||
/// | ||
/// #[derive(sqlx::FromRow)] | ||
/// struct Person { | ||
/// id: i64, | ||
/// name: String, | ||
/// birthdate: DateTime<Utc>, | ||
/// } | ||
/// | ||
/// # let people: &[Person] = people(); | ||
/// sqlx::query("insert into person(id, name, birthdate) select * from unnest($1, $2, $3)") | ||
/// .bind(people.iter().map(|p| p.id).bind_iter()) | ||
/// .bind(people.iter().map(|p| &p.name).bind_iter()) | ||
/// .bind(people.iter().map(|p| &p.birthdate).bind_iter()) | ||
/// .execute(&mut conn) | ||
/// .await?; | ||
/// | ||
/// # Ok(()) | ||
/// # } | ||
/// ``` | ||
pub trait PgBindIterExt: Iterator + Sized { | ||
fn bind_iter(self) -> PgBindIter<Self>; | ||
} | ||
|
||
impl<I: Iterator + Sized> PgBindIterExt for I { | ||
fn bind_iter(self) -> PgBindIter<I> { | ||
PgBindIter(self) | ||
} | ||
} | ||
|
||
impl<I> Type<Postgres> for PgBindIter<I> | ||
where | ||
I: Iterator, | ||
<I as Iterator>::Item: Type<Postgres> + PgHasArrayType, | ||
{ | ||
fn type_info() -> <Postgres as Database>::TypeInfo { | ||
<I as Iterator>::Item::array_type_info() | ||
} | ||
fn compatible(ty: &PgTypeInfo) -> bool { | ||
<I as Iterator>::Item::array_compatible(ty) | ||
} | ||
} | ||
|
||
impl<'q, I> PgBindIter<I> | ||
where | ||
I: Iterator, | ||
<I as Iterator>::Item: Type<Postgres> + Encode<'q, Postgres>, | ||
{ | ||
fn encode_inner( | ||
// need ownership to iterate | ||
mut iter: I, | ||
buf: &mut PgArgumentBuffer, | ||
) -> Result<IsNull, BoxDynError> { | ||
let lower_size_hint = iter.size_hint().0; | ||
let first = iter.next(); | ||
let type_info = first | ||
.as_ref() | ||
.and_then(Encode::produces) | ||
.unwrap_or_else(<I as Iterator>::Item::type_info); | ||
|
||
buf.extend(&1_i32.to_be_bytes()); // number of dimensions | ||
buf.extend(&0_i32.to_be_bytes()); // flags | ||
|
||
match type_info.0 { | ||
PgType::DeclareWithName(name) => buf.patch_type_by_name(&name), | ||
PgType::DeclareArrayOf(array) => buf.patch_array_type(array), | ||
|
||
ty => { | ||
buf.extend(&ty.oid().0.to_be_bytes()); | ||
} | ||
} | ||
|
||
let len_start = buf.len(); | ||
buf.extend(0_i32.to_be_bytes()); // len (unknown so far) | ||
buf.extend(1_i32.to_be_bytes()); // lower bound | ||
|
||
match first { | ||
Some(first) => buf.encode(first)?, | ||
None => return Ok(IsNull::No), | ||
} | ||
|
||
let mut count = 1_i32; | ||
const MAX: usize = i32::MAX as usize - 1; | ||
|
||
for value in (&mut iter).take(MAX) { | ||
buf.encode(value)?; | ||
count += 1; | ||
} | ||
|
||
const OVERFLOW: usize = i32::MAX as usize + 1; | ||
if iter.next().is_some() { | ||
let iter_size = std::cmp::max(lower_size_hint, OVERFLOW); | ||
return Err(format!("encoded iterator is too large for Postgres: {iter_size}").into()); | ||
} | ||
|
||
// set the length now that we know what it is. | ||
buf[len_start..(len_start + 4)].copy_from_slice(&count.to_be_bytes()); | ||
|
||
Ok(IsNull::No) | ||
} | ||
} | ||
|
||
impl<'q, I> Encode<'q, Postgres> for PgBindIter<I> | ||
where | ||
// Clone is required for the encode_by_ref call since we can't iterate with a shared reference | ||
I: Iterator + Clone, | ||
<I as Iterator>::Item: Type<Postgres> + Encode<'q, Postgres>, | ||
{ | ||
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> { | ||
Self::encode_inner(self.0.clone(), buf) | ||
} | ||
fn encode(self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> | ||
where | ||
Self: Sized, | ||
{ | ||
Self::encode_inner(self.0, buf) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan I had for this was to use
Cell<Option<I>>
since theClone
bound could theoretically be hard to satisfy for some iterators.Using
Cell
would make it!Sync
and only able to used once, but those seem like pretty reasonable tradeoffs. I don't expect anyone to need to share this across threads or or need to bind the same iterator more than once (they could just use the same placeholder number to refer to the existing binding or create a new iterator).It seems like maybe we should split
Encode
into two traits: one that encodes by-value and one by-reference, then a blanket impl of the former over the latter. The by-reference trait would be the one that most types would implement, but the by-value trait would be the one that all the APIs actually accept.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I can make the change. I don't think it will affect the send-ness of the execution future so that should be fine.
I agree with splitting up the Encode trait. It would make this simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@abonander I've made the change to
Cell<Option<I>>