Skip to content

Commit

Permalink
sql: retain original columns in Nothing nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
erikgrinaker committed Jul 16, 2024
1 parent b4b65c6 commit f629387
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/sql/execution/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub fn execute(node: Node, txn: &impl Transaction) -> Result<Rows> {
join::nested_loop(left, right, right_size, predicate, outer)
}

Node::Nothing => Ok(source::nothing()),
Node::Nothing { .. } => Ok(source::nothing()),

Node::Offset { source, offset } => {
let source = execute(*source, txn)?;
Expand Down
46 changes: 27 additions & 19 deletions src/sql/planner/optimizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ pub(super) fn short_circuit(node: Node) -> Result<Node> {
use Expression::Constant;
use Value::{Boolean, Null};

/// Creates a Nothing node with the columns of the original node.
fn nothing(node: &Node) -> Node {
let columns = (0..node.size()).map(|i| node.column_label(i)).collect();
Node::Nothing { columns }
}

let transform = |node| match node {
// Filter nodes that always yield true are unnecessary: remove them.
Node::Filter { source, predicate: Constant(Boolean(true)) } => *source,
Expand All @@ -300,32 +306,34 @@ pub(super) fn short_circuit(node: Node) -> Result<Node> {
}

// Short-circuit nodes that can't produce anything by replacing them
// with a Nothing node.
Node::Filter { predicate: Constant(Boolean(false) | Null), .. } => Node::Nothing,
Node::IndexLookup { values, .. } if values.is_empty() => Node::Nothing,
Node::KeyLookup { keys, .. } if keys.is_empty() => Node::Nothing,
Node::Limit { limit: 0, .. } => Node::Nothing,
Node::NestedLoopJoin { predicate: Some(Constant(Boolean(false) | Null)), .. } => {
Node::Nothing
// with a Nothing node, retaining the columns.
ref node @ Node::Filter { predicate: Constant(Boolean(false) | Null), .. } => nothing(node),
ref node @ Node::IndexLookup { ref values, .. } if values.is_empty() => nothing(node),
ref node @ Node::KeyLookup { ref keys, .. } if keys.is_empty() => nothing(node),
ref node @ Node::Limit { limit: 0, .. } => nothing(node),
ref node @ Node::NestedLoopJoin {
predicate: Some(Constant(Boolean(false) | Null)), ..
} => nothing(node),
ref node @ Node::Scan { filter: Some(Constant(Boolean(false) | Null)), .. } => {
nothing(node)
}
Node::Scan { filter: Some(Constant(Boolean(false) | Null)), .. } => Node::Nothing,
Node::Values { rows, .. } if rows.is_empty() => Node::Nothing,
Node::Values { rows } if rows.is_empty() => Node::Nothing { columns: vec![] },

// Short-circuit nodes that pull from a Nothing node.
//
// NB: does not short-circuit aggregation, since an aggregation over 0
// rows should produce a result.
Node::Filter { source, .. }
| Node::HashJoin { left: source, .. }
| Node::HashJoin { right: source, .. }
| Node::NestedLoopJoin { left: source, .. }
| Node::NestedLoopJoin { right: source, .. }
| Node::Offset { source, .. }
| Node::Order { source, .. }
| Node::Projection { source, .. }
if *source == Node::Nothing =>
ref node @ (Node::Filter { ref source, .. }
| Node::HashJoin { left: ref source, .. }
| Node::HashJoin { right: ref source, .. }
| Node::NestedLoopJoin { left: ref source, .. }
| Node::NestedLoopJoin { right: ref source, .. }
| Node::Offset { ref source, .. }
| Node::Order { ref source, .. }
| Node::Projection { ref source, .. })
if matches!(**source, Node::Nothing { .. }) =>
{
Node::Nothing
nothing(node)
}

// Remove noop projections that simply pass through the source columns.
Expand Down
20 changes: 12 additions & 8 deletions src/sql/planner/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ pub enum Node {
/// When outer is true (e.g. LEFT JOIN), a left row without a right match is
/// emitted anyway, with NULLs for the right row.
NestedLoopJoin { left: Box<Node>, right: Box<Node>, predicate: Option<Expression>, outer: bool },
/// Nothing does not emit anything.
Nothing,
/// Nothing does not emit anything, but retains the column names of any
/// replaced nodes for results and plan expression display.
Nothing { columns: Vec<Label> },
/// Discards the first offset rows from source, emits the rest.
Offset { source: Box<Node>, offset: usize },
/// Sorts the source rows by the given expression/direction pairs. Buffers
Expand Down Expand Up @@ -174,7 +175,7 @@ impl Node {

node @ (Self::IndexLookup { .. }
| Self::KeyLookup { .. }
| Self::Nothing
| Self::Nothing { .. }
| Self::Scan { .. }
| Self::Values { .. }) => node,
};
Expand Down Expand Up @@ -232,7 +233,7 @@ impl Node {
| Self::KeyLookup { .. }
| Self::Limit { .. }
| Self::NestedLoopJoin { predicate: None, .. }
| Self::Nothing
| Self::Nothing { .. }
| Self::Offset { .. }
| Self::Scan { filter: None, .. }
| Self::Remap { .. }) => node,
Expand Down Expand Up @@ -262,7 +263,7 @@ impl Node {
// Some can't have names.
Self::HashJoin { .. }
| Self::NestedLoopJoin { .. }
| Self::Nothing
| Self::Nothing { .. }
| Self::Values { .. } => None,
}
}
Expand Down Expand Up @@ -314,8 +315,11 @@ impl Node {
| Self::Offset { source, .. }
| Self::Order { source, .. } => source.column_label(index),

// Nothing nodes contain the original columns of replaced nodes.
Self::Nothing { columns } => columns.get(index).cloned().unwrap_or(Label::None),

// And some don't have any names at all.
Self::Nothing | Self::Values { .. } => Label::None,
Self::Values { .. } => Label::None,
}
}

Expand Down Expand Up @@ -346,7 +350,7 @@ impl Node {
| Self::Order { source, .. } => source.size(),

// And some are trivial.
Self::Nothing => 0,
Self::Nothing { columns } => columns.len(),
Self::Values { rows } => rows.first().map(|row| row.len()).unwrap_or(0),
}
}
Expand Down Expand Up @@ -479,7 +483,7 @@ impl Node {
left.format(f, prefix.clone(), false, false)?;
right.format(f, prefix, false, true)?;
}
Self::Nothing => {
Self::Nothing { .. } => {
write!(f, "Nothing")?;
}
Self::Offset { source, offset } => {
Expand Down
31 changes: 20 additions & 11 deletions src/sql/testscripts/optimizer/short_circuit
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ Short circuit:
3, c, 2, 2
3, c, 3, 3

# FALSE predicates → Nothing
[opt]> SELECT * FROM test WHERE FALSE
# FALSE predicates → Nothing (but retains column headers)
[opt,header]> SELECT * FROM test WHERE FALSE
---
Initial:
Filter: FALSE
Expand All @@ -66,17 +66,19 @@ Filter pushdown:
Scan: test (FALSE)
Short circuit:
Nothing
test.id, test.value

[opt]> SELECT 1, 2, 3 WHERE FALSE
[opt,header]> SELECT 1, 2, 3 WHERE FALSE
---
Initial:
Projection: 1, 2, 3
└─ Filter: FALSE
└─ Values: blank row
Short circuit:
Nothing
, ,

[opt]> SELECT * FROM test JOIN ref ON ref.test_id = test.id AND FALSE
[opt,header]> SELECT * FROM test JOIN ref ON ref.test_id = test.id AND FALSE
---
Initial:
NestedLoopJoin: inner on ref.test_id = test.id AND FALSE
Expand All @@ -92,9 +94,10 @@ Filter pushdown:
└─ Scan: ref (FALSE)
Short circuit:
Nothing
test.id, test.value, ref.id, ref.test_id

# NULL predicates → Nothing
[opt]> SELECT * FROM test WHERE NULL
[opt,header]> SELECT * FROM test WHERE NULL
---
Initial:
Filter: NULL
Expand All @@ -103,17 +106,19 @@ Filter pushdown:
Scan: test (NULL)
Short circuit:
Nothing
test.id, test.value

[opt]> SELECT 1, 2, 3 WHERE NULL
[opt,header]> SELECT 1, 2, 3 WHERE NULL
---
Initial:
Projection: 1, 2, 3
└─ Filter: NULL
└─ Values: blank row
Short circuit:
Nothing
, ,

[opt]> SELECT * FROM test JOIN ref ON ref.test_id = test.id AND NULL
[opt,header]> SELECT * FROM test JOIN ref ON ref.test_id = test.id AND NULL
---
Initial:
NestedLoopJoin: inner on ref.test_id = test.id AND NULL
Expand All @@ -129,9 +134,10 @@ Join type:
└─ Scan: ref (NULL)
Short circuit:
Nothing
test.id, test.value, ref.id, ref.test_id

# Empty key/index lookups.
[opt]> SELECT * FROM test WHERE id = NULL
# Empty key/index lookups → Nothing
[opt,header]> SELECT * FROM test WHERE id = NULL
---
Initial:
Filter: test.id = NULL
Expand All @@ -142,8 +148,9 @@ Index lookup:
KeyLookup: test (0 keys)
Short circuit:
Nothing
test.id, test.value

[opt]> SELECT * FROM ref WHERE test_id = NULL
[opt,header]> SELECT * FROM ref WHERE test_id = NULL
---
Initial:
Filter: ref.test_id = NULL
Expand All @@ -154,15 +161,17 @@ Index lookup:
IndexLookup: ref.test_id (0 values)
Short circuit:
Nothing
ref.id, ref.test_id

# LIMIT 0 → Nothing
[opt]> SELECT * FROM test LIMIT 0
[opt,header]> SELECT * FROM test LIMIT 0
---
Initial:
Limit: 0
└─ Scan: test
Short circuit:
Nothing
test.id, test.value

# Remove projections that simply pass through source columns. Aliased
# column names are retained.
Expand Down
6 changes: 6 additions & 0 deletions src/sql/testscripts/queries/select
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,9 @@ NULL, b
2, 2
3, 1
3, 2

# A select with no rows optimized to a Nothing node still emits headers.
[plan,header]> SELECT * FROM test WHERE FALSE
---
Nothing
test.id, test.bool, test.float, test.int, test.string

0 comments on commit f629387

Please sign in to comment.