constexpr size_t STANDARD_VECTOR_SIZE = 1024;
struct SelectionVector {
uint32_t indices[STANDARD_VECTOR_SIZE];
size_t count;
};
class Batch {
public:
ColumnVector& column(size_t i);
size_t num_columns() const;
size_t count() const; // logical row count
size_t base_count() const; // physical rows filled
const SelectionVector* selection() const; // nullptr = no filtering active
size_t row_index(size_t i) const; // logical -> physical
void reset();
};
class Operator {
public:
virtual void open(ExecutionContext& ctx) = 0;
virtual bool next(ExecutionContext& ctx, Batch& out) = 0; // false = EOF
virtual void close(ExecutionContext& ctx) = 0;
virtual size_t num_output_columns() const = 0;
virtual ~Operator() = default;
};
struct ExecutionContext {
Profiler profiler;
// memory accounting, cancellation, tracing live here later - not on Operator
};num_output_columns() is the v0 stand-in for output schema information - the
consumer needs it to size the root batch, and Project needs it to size its
input batch. It grows into a real schema object when types beyond int64
arrive.
ExecutionContext carries everything that is per-execution rather than
per-plan: today that is only the profiler; later it is the memory
accountant, the cancellation flag, and the tracer. Operators stay stateless
with respect to these concerns - nothing cross-cutting lives on Operator,
so adding a concern never touches operator code.
- Operators register a stats slot in
open()viactx.profiler.get("Name")and cache the returned pointer. Registration happens child-first, so the report prints in pipeline order (Scan, Filter, Project). No string lookup ever happens insidenext(). - Per-slot stats: exclusive time (ns), calls, rows in, rows out.
ScopedTimeris RAII around the timed region. Whenprofiler.enabled == falseit compiles down to a null-pointer check - the benchmark's headline numbers come from unprofiled runs.
Each operator times only its own work, not its child's. This falls out of where the timer is placed rather than from subtraction at report time:
Scan::nexttimes its whole body (it has no child).Filter::nextstarts its timer afterchild->next()returns.Project::nextstarts its timer after its input batch is filled.
So the per-operator table is exclusive time and the columns sum (approximately) to the query total.
Caveat: with batch_size = 1, a profiled run performs two clock reads per
operator per tuple - tens of millions of clock calls. Profiled runs are for
the shape of the operator table (rows in/out, relative cost); headline
timings always come from unprofiled runs.