Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 122 additions & 28 deletions datafusion/expr/src/physical_planning_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,70 +123,150 @@ impl SubqueryIndex {
}
}

/// An alternate backing store for a [`ScalarSubqueryResults`] container.
///
/// A results container normally owns its slots directly (see
/// [`ScalarSubqueryResults::new`]). This trait lets a container instead
/// forward `get`/`set`/`clear` to some other shared container, which is how
/// `datafusion-ffi` bridges the results of a `ScalarSubqueryExec` across a
/// non-Rust-ABI boundary: the far side of the boundary gets a
/// [`ScalarSubqueryResults`] backed by a proxy that calls back into the
/// near side's real container for every operation, so both sides observe the
/// same populated values.
pub trait ScalarSubqueryResultsBackend:
fmt::Debug + Send + Sync + std::panic::RefUnwindSafe
{
/// Returns the scalar value stored at `index`, if it has been populated.
fn get(&self, index: usize) -> Option<ScalarValue>;

/// Stores `value` in the slot at `index`.
fn set(&self, index: usize, value: ScalarValue) -> Result<()>;

/// Clears all populated results so the container can be reused.
fn clear(&self);
}

#[derive(Clone)]
enum ScalarSubqueryResultsRepr {
Local(Arc<Vec<Mutex<Option<ScalarValue>>>>),
Remote(Arc<dyn ScalarSubqueryResultsBackend>),
}

/// Shared results container for uncorrelated scalar subqueries.
///
/// Each entry corresponds to one scalar subquery, identified by its index.
/// Each slot is populated at execution time by `ScalarSubqueryExec`, read by
/// `ScalarSubqueryExpr` instances that share this container, and cleared when
/// the plan is reset for re-execution.
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct ScalarSubqueryResults {
slots: Arc<Vec<Mutex<Option<ScalarValue>>>>,
repr: ScalarSubqueryResultsRepr,
}

impl Default for ScalarSubqueryResults {
fn default() -> Self {
Self::new(0)
}
}

impl ScalarSubqueryResults {
/// Creates a new shared results container with `n` empty slots.
pub fn new(n: usize) -> Self {
Self {
slots: Arc::new((0..n).map(|_| Mutex::new(None)).collect()),
repr: ScalarSubqueryResultsRepr::Local(Arc::new(
(0..n).map(|_| Mutex::new(None)).collect(),
)),
}
}

/// Creates a results container that forwards every operation to
/// `backend`, for example a proxy that reaches across an FFI boundary to
/// a real container owned by the other side.
pub fn from_backend(backend: Arc<dyn ScalarSubqueryResultsBackend>) -> Self {
Self {
repr: ScalarSubqueryResultsRepr::Remote(backend),
}
}

/// Returns the scalar value stored at `index`, if it has been populated.
pub fn get(&self, index: SubqueryIndex) -> Option<ScalarValue> {
let slot = self.slots.get(index.as_usize())?;
slot.lock().unwrap().clone()
match &self.repr {
ScalarSubqueryResultsRepr::Local(slots) => {
let slot = slots.get(index.as_usize())?;
slot.lock().unwrap().clone()
}
ScalarSubqueryResultsRepr::Remote(backend) => backend.get(index.as_usize()),
}
}

/// Stores `value` in the slot at `index`.
pub fn set(&self, index: SubqueryIndex, value: ScalarValue) -> Result<()> {
let Some(slot) = self.slots.get(index.as_usize()) else {
return internal_err!(
"ScalarSubqueryResults: result index {} is out of bounds",
index.as_usize()
);
};

let mut slot = slot.lock().unwrap();
if slot.is_some() {
return internal_err!(
"ScalarSubqueryResults: result for index {} was already populated",
index.as_usize()
);
match &self.repr {
ScalarSubqueryResultsRepr::Local(slots) => {
let Some(slot) = slots.get(index.as_usize()) else {
return internal_err!(
"ScalarSubqueryResults: result index {} is out of bounds",
index.as_usize()
);
};

let mut slot = slot.lock().unwrap();
if slot.is_some() {
return internal_err!(
"ScalarSubqueryResults: result for index {} was already populated",
index.as_usize()
);
}
*slot = Some(value);

Ok(())
}
ScalarSubqueryResultsRepr::Remote(backend) => {
backend.set(index.as_usize(), value)
}
}
*slot = Some(value);

Ok(())
}

/// Clears all populated results so the container can be reused.
pub fn clear(&self) {
for slot in self.slots.iter() {
*slot.lock().unwrap() = None;
match &self.repr {
ScalarSubqueryResultsRepr::Local(slots) => {
for slot in slots.iter() {
*slot.lock().unwrap() = None;
}
}
ScalarSubqueryResultsRepr::Remote(backend) => backend.clear(),
}
}

/// Returns true if `this` and `other` point to the same shared container.
pub fn ptr_eq(this: &Self, other: &Self) -> bool {
Arc::ptr_eq(&this.slots, &other.slots)
match (&this.repr, &other.repr) {
(
ScalarSubqueryResultsRepr::Local(a),
ScalarSubqueryResultsRepr::Local(b),
) => Arc::ptr_eq(a, b),
(
ScalarSubqueryResultsRepr::Remote(a),
ScalarSubqueryResultsRepr::Remote(b),
) => Arc::ptr_eq(a, b),
_ => false,
}
}
}

impl fmt::Debug for ScalarSubqueryResults {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.slots.iter().map(|slot| slot.lock().unwrap().clone()))
.finish()
match &self.repr {
ScalarSubqueryResultsRepr::Local(slots) => f
.debug_list()
.entries(slots.iter().map(|slot| slot.lock().unwrap().clone()))
.finish(),
ScalarSubqueryResultsRepr::Remote(backend) => f
.debug_tuple("ScalarSubqueryResults::Remote")
.field(backend)
.finish(),
}
}
}

Expand All @@ -200,14 +280,28 @@ impl Eq for ScalarSubqueryResults {}

impl Hash for ScalarSubqueryResults {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.slots).hash(state);
match &self.repr {
ScalarSubqueryResultsRepr::Local(slots) => Arc::as_ptr(slots).hash(state),
ScalarSubqueryResultsRepr::Remote(backend) => {
Arc::as_ptr(backend).hash(state)
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;

/// `ScalarSubqueryResults` must stay `UnwindSafe`/`RefUnwindSafe` so a
/// `ScalarSubqueryResultsBackend` implementation (e.g. `datafusion-ffi`'s
/// FFI proxy) doesn't regress these auto traits for callers.
#[test]
fn scalar_subquery_results_is_unwind_safe() {
fn assert_bounds<T: std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
assert_bounds::<ScalarSubqueryResults>();
}

#[test]
fn scalar_subquery_results_set_and_get() -> Result<()> {
let results = ScalarSubqueryResults::new(1);
Expand Down
1 change: 1 addition & 0 deletions datafusion/ffi/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@

pub mod logical_extension_codec;
pub mod physical_extension_codec;
pub mod scalar_subquery_results;
Loading
Loading