6767 * NULL. Per-accessor NULL behaviour is not repeated in method docs; this
6868 * paragraph is the contract.
6969 * <p>
70- * <strong>Four zero-allocation idioms</strong> -- pick whichever matches the
70+ * <strong>Five zero-allocation idioms</strong> -- pick whichever matches the
7171 * wire type of the column:
7272 * <ul>
7373 * <li><b>Primitive accessors</b> ({@link #getLongValue}, {@link #getIntValue},
8686 * {@link #getSymbolId}) materialises each distinct SYMBOL dict entry into
8787 * a {@link String} at most once per batch, regardless of how many rows
8888 * reference it.</li>
89+ * <li><b>Row / column views</b> ({@link #row(int)}, {@link #column(int)},
90+ * {@link #forEachRow(RowCallback)}) wrap the {@code (col, row)} primitives
91+ * with single-arg accessors. {@link RowView} pins the current row;
92+ * {@link ColumnView} pins the column once and exposes a direct-address
93+ * surface ({@link ColumnView#valuesAddr()}, {@link ColumnView#nullBitmapAddr()},
94+ * etc.) for SIMD or JNI consumers. Both flyweights are batch-owned and
95+ * reused.</li>
8996 * </ul>
9097 * The heap-allocating convenience accessors {@link #getString(int, int)},
9198 * {@link #getBinary(int, int)}, and {@link #getDoubleArrayElements(int, int)}
@@ -97,6 +104,10 @@ public class QwpColumnBatch {
97104 // BINARY views -- re-pointed per call, never re-allocated.
98105 private final DirectByteSlice binaryA = new DirectByteSlice ();
99106 private final DirectByteSlice binaryB = new DirectByteSlice ();
107+ // One ColumnView per column index, lazily created by column(int). Slots are
108+ // re-pointed in place across batches; the list grows but never shrinks so a
109+ // wide-then-narrow query sequence keeps reusing the same instances.
110+ private final ObjList <ColumnView > columnViews = new ObjList <>();
100111 // Reusable views for zero-alloc UTF-8 access. strA and strB are dual views
101112 // (same pattern as QuestDB Record.getStrA/getStrB) so callers can compare
102113 // two cells without one overwriting the other.
@@ -109,6 +120,8 @@ public class QwpColumnBatch {
109120 long payloadLimit ;
110121 long requestId ;
111122 int rowCount ;
123+ // Lazily created on first row(int) / forEachRow call; reused for the batch lifetime.
124+ private RowView rowView ;
112125
113126 /**
114127 * Server-assigned monotonic sequence number for this batch within the query.
@@ -119,6 +132,43 @@ public long batchSeq() {
119132 return batchSeq ;
120133 }
121134
135+ /**
136+ * Returns the {@link ColumnView} for {@code col}, pinned and ready to read.
137+ * The view is owned by the batch and cached per column index, so calling
138+ * {@code column(0)} and {@code column(1)} returns two distinct instances
139+ * that can be held side-by-side. Calling {@code column(c)} a second time
140+ * with the same {@code c} returns the same instance with its layout
141+ * pointer refreshed against the current batch state.
142+ */
143+ public ColumnView column (int col ) {
144+ if (columnViews .size () <= col ) {
145+ columnViews .setPos (col + 1 );
146+ }
147+ ColumnView view = columnViews .getQuick (col );
148+ if (view == null ) {
149+ view = new ColumnView (this );
150+ columnViews .setQuick (col , view );
151+ }
152+ return view .of (col );
153+ }
154+
155+ /**
156+ * Iterates rows in this batch and invokes {@code callback} for each one.
157+ * The {@link RowView} handed to the callback is re-pointed in place across
158+ * iterations -- do not retain it past {@link RowCallback#onRow(RowView)}.
159+ * Throwing from the callback aborts iteration and propagates the exception.
160+ */
161+ public void forEachRow (RowCallback callback ) {
162+ if (rowView == null ) {
163+ rowView = new RowView (this );
164+ }
165+ int n = rowCount ;
166+ for (int r = 0 ; r < n ; r ++) {
167+ rowView .of (r );
168+ callback .onRow (rowView );
169+ }
170+ }
171+
122172 /**
123173 * Returns the dimensionality of the ARRAY value at {@code (col, row)}, or 0 if
124174 * the row is null. Caller must know the column is an ARRAY type.
@@ -584,6 +634,19 @@ public long requestId() {
584634 return requestId ;
585635 }
586636
637+ /**
638+ * Returns the cached {@link RowView} pointed at {@code row}. Use as an
639+ * escape hatch from {@link #forEachRow(RowCallback)} when you need to
640+ * drive iteration manually (e.g., interleaved with another data source).
641+ * Single shared instance per batch; two calls overwrite each other.
642+ */
643+ public RowView row (int row ) {
644+ if (rowView == null ) {
645+ rowView = new RowView (this );
646+ }
647+ return rowView .of (row );
648+ }
649+
587650 /**
588651 * Address of the column's packed non-null values in the payload buffer.
589652 * Layout depends on the wire type:
@@ -605,8 +668,9 @@ public long valuesAddr(int col) {
605668 * Fast null check once the layout is in hand. Inlining pattern used by all the
606669 * typed accessors: load layout once, check bitmap, read value. Eliminates the
607670 * second {@code ObjList.getQuick(col)} that separate {@code isNull(col,row)} would cost.
671+ * Also reused by {@link ColumnView} so it can keep the layout pointer cached.
608672 */
609- private static boolean isLayoutNull (QwpColumnLayout l , int row ) {
673+ static boolean isLayoutNull (QwpColumnLayout l , int row ) {
610674 if (l .nullBitmapAddr == 0 ) return false ;
611675 byte bm = Unsafe .getUnsafe ().getByte (l .nullBitmapAddr + (row >>> 3 ));
612676 return (bm & (1 << (row & 7 ))) != 0 ;
0 commit comments