Repository: duckdb/duckdb
File: src/optimizer/projection_pullup.cpp (C++)

Code before:
void ProjectionPullup::PopParents(const LogicalOperator &op) {
	// pop back elements until the last operator in the stack is THIS operator
	while (!parents.empty() && &parents.back().get() != &op) {
		parents.pop_back();
	}
	// then pop THIS operator back, and stop
	if (!parents.empty()) {
		parents.pop_back();
	}
}

void ProjectionPullup::Optimize(unique_ptr<LogicalOperator> &op) {
	switch (op->type) {
	// These operators depend on column order, so we can't remove projections below them
	case LogicalOperatorType::LOGICAL_DISTINCT:
	case LogicalOperatorType::LOGICAL_RECURSIVE_CTE:
	case LogicalOperatorType::LOGICAL_MATERIALIZED_CTE:
	case LogicalOperatorType::LOGICAL_CTE_REF:
	case LogicalOperatorType::LOGICAL_COPY_TO_FILE:
	case LogicalOperatorType::LOGICAL_PIVOT:
	case LogicalOperatorType::LOGICAL_UNION:
	case LogicalOperatorType::LOGICAL_EXCEPT:
	case LogicalOperatorType::LOGICAL_INTERSECT: {
		return;
	}
	case LogicalOperatorType::LOGICAL_ANY_JOIN:
	case LogicalOperatorType::LOGICAL_COMPARISON_JOIN: {
		auto &comp_join = op->Cast<LogicalComparisonJoin>();
		// FIXME: for now, skip pulling up projections through SEMI.
		// Improvement: skip removing projections on the LHS of semi joins
		if (comp_join.join_type == JoinType::MARK || comp_join.join_type == JoinType::SEMI) {
			break; // bail
		}

		// We can pull through this operator, add it to the stack
		parents.push_back(*op);

		// Recurse
		for (auto &child : op->children) {
			Optimize(child);
		}

		PopParents(*op);
		return;
	}
	case LogicalOperatorType::LOGICAL_FILTER: {
		// We can pull through this operator, add it to the stack
		parents.push_back(*op);

		// Recurse
		Optimize(op->children[0]);

Review comment (suggestion):
This immediately bails on on recursion. Maybe, nested down later in the plan, we might be able to remove projections again. I think if you want to tackle this in a generic way, you can skip only the projection that is right under this operator, and then the recursion should handle rest. If there is no projection under this operator, you can create one, skip it, and recurse. Happy to chat about this in more detail if needed :)

Code after:
void ProjectionPullup::PopParents(const LogicalOperator &op) {
	// pop back elements until the last operator in the stack is THIS operator
	while (!parents.empty() && &parents.back().get() != &op) {
		parents.pop_back();
	}
	// then pop THIS operator back, and stop
	if (!parents.empty()) {
		parents.pop_back();
	}
}

void ProjectionPullup::Optimize(unique_ptr<LogicalOperator> &op) {
	switch (op->type) {
	// These operators depend on column order, so we can't remove projections below them
	case LogicalOperatorType::LOGICAL_DISTINCT:
	case LogicalOperatorType::LOGICAL_RECURSIVE_CTE:
	case LogicalOperatorType::LOGICAL_MATERIALIZED_CTE:
	case LogicalOperatorType::LOGICAL_CTE_REF:
	case LogicalOperatorType::LOGICAL_COPY_TO_FILE:
	case LogicalOperatorType::LOGICAL_PIVOT:
	case LogicalOperatorType::LOGICAL_UNION:
	case LogicalOperatorType::LOGICAL_EXCEPT:
	case LogicalOperatorType::LOGICAL_INTERSECT: {
		// FIXME: Do not bail out completely. Do not remove projections directly below these operators. Deeper layers of
		// the plan should be able to be optimized further
		return;
	}
	case LogicalOperatorType::LOGICAL_ANY_JOIN:
	case LogicalOperatorType::LOGICAL_COMPARISON_JOIN: {
		auto &comp_join = op->Cast<LogicalComparisonJoin>();
		// FIXME: For SEMI, we should be able to recurse into the LHS sub-tree with no problem. For the RHS, do not
		// remove projections directly below it.
		if (comp_join.join_type == JoinType::MARK || comp_join.join_type == JoinType::SEMI) {
			break; // bail
		}

		// We can pull through this operator, add it to the stack
		parents.push_back(*op);

		// Recurse
		for (auto &child : op->children) {
			Optimize(child);
		}

		PopParents(*op);
		return;
	}
	case LogicalOperatorType::LOGICAL_FILTER: {
		// We can pull through this operator, add it to the stack
		parents.push_back(*op);