UI: move tracks to queryV2

Use the new streaming query operator for all
tracks. Some controllers still need update.

Change-Id: Iec3dcdf4d9432636aec0a9a941fee986587172c7
Bug: 159142289
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index f56c8aa..6d77b0f 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -24,7 +24,7 @@
   RawQueryArgs,
   RawQueryResult
 } from './protos';
-import {iter, NUM_NULL, slowlyCountRows, STR} from './query_iterator';
+import {NUM, NUM_NULL, slowlyCountRows, STR} from './query_iterator';
 import {
   createQueryResult,
   QueryResult,
@@ -365,10 +365,13 @@
   // TODO(hjd): When streaming must invalidate this somehow.
   async getCpus(): Promise<number[]> {
     if (!this._cpus) {
-      const result =
-          await this.query('select distinct(cpu) from sched order by cpu;');
-      if (slowlyCountRows(result) === 0) return [];
-      this._cpus = result.columns[0].longValues!.map(n => +n);
+      const cpus = [];
+      const queryRes = await this.queryV2(
+          'select distinct(cpu) as cpu from sched order by cpu;');
+      for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+        cpus.push(it.cpu);
+      }
+      this._cpus = cpus;
     }
     return this._cpus;
   }
@@ -388,8 +391,8 @@
   // TODO: This should live in code that's more specific to chrome, instead of
   // in engine.
   async getNumberOfProcesses(): Promise<number> {
-    const result = await this.query('select count(*) from process;');
-    return +result.columns[0].longValues![0];
+    const result = await this.queryV2('select count(*) as cnt from process;');
+    return result.firstRow({cnt: NUM}).cnt;
   }
 
   async getTraceTimeBounds(): Promise<TimeSpan> {
@@ -399,15 +402,15 @@
   }
 
   async getTracingMetadataTimeBounds(): Promise<TimeSpan> {
-    const query = await this.query(`select name, int_value from metadata
+    const queryRes = await this.queryV2(`select name, int_value from metadata
          where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
          or name = 'all_data_source_started_ns'`);
     let startBound = -Infinity;
     let endBound = Infinity;
-    const it = iter({'name': STR, 'int_value': NUM_NULL}, query);
+    const it = queryRes.iter({'name': STR, 'int_value': NUM_NULL});
     for (; it.valid(); it.next()) {
-      const columnName = it.row.name;
-      const timestamp = it.row.int_value;
+      const columnName = it.name;
+      const timestamp = it.int_value;
       if (timestamp === null) continue;
       if (columnName === 'tracing_disabled_ns') {
         endBound = Math.min(endBound, timestamp / 1e9);
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
index 20c9424..82afc75 100644
--- a/ui/src/common/query_result.ts
+++ b/ui/src/common/query_result.ts
@@ -100,6 +100,11 @@
   // iter<T extends Row>(spec: T): RowIterator<T>;
   iter<T extends Row>(spec: T): RowIterator<T>;
 
+  // Like iter() for queries that expect only one row. It embeds the valid()
+  // check (i.e. throws if no rows are available) and returns directly the
+  // first result.
+  firstRow<T extends Row>(spec: T): T;
+
   // If != undefined the query errored out and error() contains the message.
   error(): string|undefined;
 
@@ -179,6 +184,12 @@
     return impl as {} as RowIterator<T>;
   }
 
+  firstRow<T extends Row>(spec: T): T {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    assertTrue(impl.valid());
+    return impl as {} as RowIterator<T>as T;
+  }
+
   // Can be called only once.
   waitAllRows(): Promise<QueryResult> {
     assertTrue(this.allRowsPromise === undefined);
@@ -510,6 +521,7 @@
     if (nextBatchIdx >= this.resultObj.batches.length) {
       return false;
     }
+
     this.columnNames = this.resultObj.columnNames;
     this.numColumns = this.columnNames.length;
 
@@ -582,7 +594,8 @@
 // This is the object ultimately returned to the client when calling
 // QueryResult.iter(...).
 // The only reason why this is disjoint from RowIteratorImpl is to avoid
-// polluting the class members with the state variables required by
+// naming collisions between the members variables required by RowIteratorImpl
+// and the column names returned by the iterator.
 class RowIteratorImplWithRowData implements RowIteratorBase {
   private _impl: RowIteratorImpl;
 
@@ -613,6 +626,9 @@
   iter<T extends Row>(spec: T) {
      return this.impl.iter(spec);
   }
+  firstRow<T extends Row>(spec: T) {
+     return this.impl.firstRow(spec);
+  }
   waitAllRows() {
      return this.impl.waitAllRows();
   }
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index 06f2d7a..8f71701 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -14,7 +14,7 @@
 
 import {TRACE_MARGIN_TIME_S} from '../common/constants';
 import {Engine} from '../common/engine';
-import {slowlyCountRows} from '../common/query_iterator';
+import {NUM, STR} from '../common/query_iterator';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {TimeSpan} from '../common/time';
 
@@ -59,11 +59,11 @@
   }
 
   private async setup() {
-    await this.query(`create virtual table search_summary_window
+    await this.queryV2(`create virtual table search_summary_window
       using window;`);
-    await this.query(`create virtual table search_summary_sched_span using
+    await this.queryV2(`create virtual table search_summary_sched_span using
       span_join(sched PARTITIONED cpu, search_summary_window);`);
-    await this.query(`create virtual table search_summary_slice_span using
+    await this.queryV2(`create virtual table search_summary_slice_span using
       span_join(slice PARTITIONED track_id, search_summary_window);`);
   }
 
@@ -151,22 +151,25 @@
 
     startNs = Math.floor(startNs / quantumNs) * quantumNs;
 
-    await this.query(`update search_summary_window set
+    await this.queryV2(`update search_summary_window set
       window_start=${startNs},
       window_dur=${endNs - startNs},
       quantum=${quantumNs}
       where rowid = 0;`);
 
-    const rawUtidResult = await this.query(`select utid from thread join process
+    const utidRes = await this.queryV2(`select utid from thread join process
       using(upid) where thread.name like ${searchLiteral}
       or process.name like ${searchLiteral}`);
 
-    const utids = [...rawUtidResult.columns[0].longValues!];
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
 
     const cpus = await this.engine.getCpus();
     const maxCpu = Math.max(...cpus, -1);
 
-    const rawResult = await this.query(`
+    const res = await this.queryV2(`
         select
           (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart,
           ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 as tsEnd,
@@ -185,18 +188,18 @@
           group by quantum_ts
           order by quantum_ts;`);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = res.numRows();
     const summary = {
       tsStarts: new Float64Array(numRows),
       tsEnds: new Float64Array(numRows),
       count: new Uint8Array(numRows)
     };
 
-    const columns = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      summary.tsStarts[row] = +columns[0].doubleValues![row];
-      summary.tsEnds[row] = +columns[1].doubleValues![row];
-      summary.count[row] = +columns[2].longValues![row];
+    const it = res.iter({tsStart: NUM, tsEnd: NUM, count: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      summary.tsStarts[row] = it.tsStart;
+      summary.tsEnds[row] = it.tsEnd;
+      summary.count[row] = it.count;
     }
     return summary;
   }
@@ -226,80 +229,78 @@
       }
     }
 
-    const rawUtidResult = await this.query(`select utid from thread join process
+    const utidRes = await this.queryV2(`select utid from thread join process
     using(upid) where
       thread.name like ${searchLiteral} or
       process.name like ${searchLiteral}`);
-    const utids = [...rawUtidResult.columns[0].longValues!];
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
     select
-      id as slice_id,
+      id as sliceId,
       ts,
       'cpu' as source,
-      cpu as source_id,
+      cpu as sourceId,
       utid
     from sched where utid in (${utids.join(',')})
     union
     select
-      slice_id,
+      slice_id as sliceId,
       ts,
       'track' as source,
-      track_id as source_id,
+      track_id as sourceId,
       0 as utid
       from slice
       where slice.name like ${searchLiteral}
     union
     select
-      slice_id,
+      slice_id as sliceId,
       ts,
       'track' as source,
-      track_id as source_id,
+      track_id as sourceId,
       0 as utid
       from slice
       join args using(arg_set_id)
       where string_value like ${searchLiteral}
     order by ts`);
 
-    const numRows = slowlyCountRows(rawResult);
-
     const searchResults: CurrentSearchResults = {
       sliceIds: [],
       tsStarts: [],
       utids: [],
       trackIds: [],
       sources: [],
-      totalResults: +numRows,
+      totalResults: queryRes.numRows(),
     };
 
-    const columns = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const source = columns[2].stringValues![row];
-      const sourceId = +columns[3].longValues![row];
+    const spec = {sliceId: NUM, ts: NUM, source: STR, sourceId: NUM, utid: NUM};
+    for (const it = queryRes.iter(spec); it.valid(); it.next()) {
       let trackId = undefined;
-      if (source === 'cpu') {
-        trackId = cpuToTrackId.get(sourceId);
-      } else if (source === 'track') {
-        trackId = engineTrackIdToTrackId.get(sourceId);
+      if (it.source === 'cpu') {
+        trackId = cpuToTrackId.get(it.sourceId);
+      } else if (it.source === 'track') {
+        trackId = engineTrackIdToTrackId.get(it.sourceId);
       }
 
+      // The .get() calls above could return undefined, this isn't just an else.
       if (trackId === undefined) {
         searchResults.totalResults--;
         continue;
       }
-
       searchResults.trackIds.push(trackId);
-      searchResults.sources.push(source);
-      searchResults.sliceIds.push(+columns[0].longValues![row]);
-      searchResults.tsStarts.push(+columns[1].longValues![row]);
-      searchResults.utids.push(+columns[4].longValues![row]);
+      searchResults.sources.push(it.source);
+      searchResults.sliceIds.push(it.sliceId);
+      searchResults.tsStarts.push(it.ts);
+      searchResults.utids.push(it.utid);
     }
     return searchResults;
   }
 
-
-  private async query(query: string) {
-    const result = await this.engine.query(query);
+  private async queryV2(query: string) {
+    const result = await this.engine.queryV2(query);
     return result;
   }
 }
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index b3742b3..b2a82fb 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -118,6 +118,11 @@
     return result;
   }
 
+  protected async queryV2(query: string) {
+    const result = await this.engine.queryV2(query);
+    return result;
+  }
+
   private shouldReload(): boolean {
     const {lastTrackReloadRequest} = globals.state;
     return !!lastTrackReloadRequest &&
diff --git a/ui/src/tracks/actual_frames/controller.ts b/ui/src/tracks/actual_frames/controller.ts
index 58140f7..95c9201 100644
--- a/ui/src/tracks/actual_frames/controller.ts
+++ b/ui/src/tracks/actual_frames/controller.ts
@@ -12,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists, assertTrue} from '../../base/logging';
-import {
-  iter,
-  NUM,
-  singleRow,
-  slowlyCountRows,
-  STR
-} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -51,18 +45,17 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
         select
           max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
             as maxDur
         from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      const row = singleRow({maxDur: NUM}, maxDurResult);
-      this.maxDurNs = assertExists(row).maxDur;
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const rawResult = await this.queryV2(`
       SELECT
         (s.ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         s.ts as ts,
@@ -92,7 +85,7 @@
       order by tsq, s.layout_depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = rawResult.numRows();
     const slices: Data = {
       start,
       end,
@@ -119,23 +112,21 @@
       return idx;
     }
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'ts': NUM,
-          'dur': NUM,
-          'layoutDepth': NUM,
-          'id': NUM,
-          'name': STR,
-          'isInstant': NUM,
-          'isIncomplete': NUM,
-          'color': STR,
-        },
-        rawResult);
+    const it = rawResult.iter({
+      'tsq': NUM,
+      'ts': NUM,
+      'dur': NUM,
+      'layoutDepth': NUM,
+      'id': NUM,
+      'name': STR,
+      'isInstant': NUM,
+      'isIncomplete': NUM,
+      'color': STR,
+    });
     for (let i = 0; it.valid(); i++, it.next()) {
-      const startNsQ = it.row.tsq;
-      const startNs = it.row.ts;
-      const durNs = it.row.dur;
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
@@ -145,12 +136,12 @@
 
       slices.starts[i] = fromNs(startNsQ);
       slices.ends[i] = fromNs(endNsQ);
-      slices.depths[i] = it.row.layoutDepth;
-      slices.titles[i] = internString(it.row.name);
-      slices.colors![i] = internString(it.row.color);
-      slices.sliceIds[i] = it.row.id;
-      slices.isInstant[i] = it.row.isInstant;
-      slices.isIncomplete[i] = it.row.isIncomplete;
+      slices.depths[i] = it.layoutDepth;
+      slices.titles[i] = internString(it.name);
+      slices.colors![i] = internString(it.color);
+      slices.sliceIds[i] = it.id;
+      slices.isInstant[i] = it.isInstant;
+      slices.isIncomplete[i] = it.isIncomplete;
     }
     return slices;
   }
diff --git a/ui/src/tracks/android_log/controller.ts b/ui/src/tracks/android_log/controller.ts
index afd235c..3f1401a 100644
--- a/ui/src/tracks/android_log/controller.ts
+++ b/ui/src/tracks/android_log/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNsCeil, toNsFloor} from '../../common/time';
 import {LIMIT} from '../../common/track_data';
 import {
@@ -33,17 +33,17 @@
     // |resolution| is in s/px the frontend wants.
     const quantNs = toNsCeil(resolution);
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
-        cast(ts / ${quantNs} as integer) * ${quantNs} as ts_quant,
+        cast(ts / ${quantNs} as integer) * ${quantNs} as tsQuant,
         prio,
-        count(prio)
+        count(prio) as numEvents
       from android_logs
       where ts >= ${startNs} and ts <= ${endNs}
-      group by ts_quant, prio
-      order by ts_quant, prio limit ${LIMIT};`);
+      group by tsQuant, prio
+      order by tsQuant, prio limit ${LIMIT};`);
 
-    const rowCount = slowlyCountRows(rawResult);
+    const rowCount = queryRes.numRows();
     const result = {
       start,
       end,
@@ -53,12 +53,14 @@
       timestamps: new Float64Array(rowCount),
       priorities: new Uint8Array(rowCount),
     };
-    const cols = rawResult.columns;
-    for (let i = 0; i < rowCount; i++) {
-      result.timestamps[i] = fromNs(+cols[0].longValues![i]);
-      const prio = Math.min(+cols[1].longValues![i], 7);
-      result.priorities[i] |= (1 << prio);
-      result.numEvents += +cols[2].longValues![i];
+
+
+    const it = queryRes.iter({tsQuant: NUM, prio: NUM, numEvents: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      result.timestamps[row] = fromNs(it.tsQuant);
+      const prio = Math.min(it.prio, 7);
+      result.priorities[row] |= (1 << prio);
+      result.numEvents += it.numEvents;
     }
     return result;
   }
diff --git a/ui/src/tracks/async_slices/controller.ts b/ui/src/tracks/async_slices/controller.ts
index 50a074a..0cfc609 100644
--- a/ui/src/tracks/async_slices/controller.ts
+++ b/ui/src/tracks/async_slices/controller.ts
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -37,36 +38,34 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
         select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-        from experimental_slice_layout
+        as maxDur from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
         max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
-        layout_depth,
+        layout_depth as depth,
         name,
         id,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       from experimental_slice_layout
       where
         filter_track_ids = '${this.config.trackIds.join(',')}' and
         ts >= ${startNs - this.maxDurNs} and
         ts <= ${endNs}
-      group by tsq, layout_depth
-      order by tsq, layout_depth
+      group by tsq, depth
+      order by tsq, depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -92,31 +91,37 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      depth: NUM,
+      name: STR,
+      id: NUM,
+      isInstant: NUM,
+      isIncomplete: NUM
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.titles[row] = internString(cols[4].stringValues![row]);
-      slices.sliceIds[row] = +cols[5].longValues![row];
-      slices.isInstant[row] = +cols[6].longValues![row];
-      slices.isIncomplete[row] = +cols[7].longValues![row];
+      slices.depths[row] = it.depth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
     }
     return slices;
   }
 }
 
-
 trackControllerRegistry.register(AsyncSliceTrackController);
diff --git a/ui/src/tracks/chrome_slices/controller.ts b/ui/src/tracks/chrome_slices/controller.ts
index 2f29388..e702fd6 100644
--- a/ui/src/tracks/chrome_slices/controller.ts
+++ b/ui/src/tracks/chrome_slices/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -41,31 +41,29 @@
     if (this.maxDurNs === 0) {
       const query = `
           SELECT max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-          FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
-      const rawResult = await this.query(query);
-      if (slowlyCountRows(rawResult) === 1) {
-        this.maxDurNs = rawResult.columns[0].longValues![0];
-      }
+          AS maxDur FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
+      const queryRes = await this.queryV2(query);
+      this.maxDurNs = queryRes.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
     const query = `
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
-        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)),
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
         depth,
-        id as slice_id,
+        id as sliceId,
         name,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       FROM ${tableName}
       WHERE track_id = ${this.config.trackId} AND
         ts >= (${startNs - this.maxDurNs}) AND
         ts <= ${endNs}
       GROUP BY depth, tsq`;
-    const rawResult = await this.query(query);
+    const queryRes = await this.queryV2(query);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -91,19 +89,26 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      depth: NUM,
+      sliceId: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
-      const isInstant = +cols[6].longValues![row];
-      const isIncomplete = +cols[7].longValues![row];
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (!isInstant && startNsQ === endNsQ) {
+      if (!it.isInstant && startNsQ === endNsQ) {
         throw new Error(
             'Expected startNsQ and endNsQ to differ (' +
             `startNsQ: ${startNsQ}, startNs: ${startNs},` +
@@ -113,11 +118,11 @@
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.sliceIds[row] = +cols[4].longValues![row];
-      slices.titles[row] = internString(cols[5].stringValues![row]);
-      slices.isInstant[row] = isInstant;
-      slices.isIncomplete[row] = isIncomplete;
+      slices.depths[row] = it.depth;
+      slices.sliceIds[row] = it.sliceId;
+      slices.titles[row] = internString(it.name);
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
     }
     return slices;
   }
diff --git a/ui/src/tracks/counter/controller.ts b/ui/src/tracks/counter/controller.ts
index 22dffe2..5b230f3 100644
--- a/ui/src/tracks/counter/controller.ts
+++ b/ui/src/tracks/counter/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -47,7 +47,7 @@
 
     if (!this.setup) {
       if (this.config.namespace === undefined) {
-        await this.query(`
+        await this.queryV2(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -59,7 +59,7 @@
           where track_id = ${this.config.trackId};
         `);
       } else {
-        await this.query(`
+        await this.queryV2(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -72,33 +72,33 @@
         `);
       }
 
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
           select
             max(
               iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
-            )
+            ) as maxDur
           from ${this.tableName('counter_view')}
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
 
-      const result = await this.query(`
+      const queryRes = await this.queryV2(`
         select
-          max(value) as maxValue,
-          min(value) as minValue,
-          max(delta) as maxDelta,
-          min(delta) as minDelta
+          ifnull(max(value), 0) as maxValue,
+          ifnull(min(value), 0) as minValue,
+          ifnull(max(delta), 0) as maxDelta,
+          ifnull(min(delta), 0) as minDelta
         from ${this.tableName('counter_view')}`);
-      this.maximumValueSeen = +result.columns[0].doubleValues![0];
-      this.minimumValueSeen = +result.columns[1].doubleValues![0];
-      this.maximumDeltaSeen = +result.columns[2].doubleValues![0];
-      this.minimumDeltaSeen = +result.columns[3].doubleValues![0];
+      const row = queryRes.firstRow(
+          {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
+      this.maximumValueSeen = row.maxValue;
+      this.minimumValueSeen = row.minValue;
+      this.maximumDeltaSeen = row.maxDelta;
+      this.minimumDeltaSeen = row.minDelta;
 
       this.setup = true;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         min(value) as minValue,
@@ -112,7 +112,7 @@
       order by tsq
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
 
     const data: Data = {
       start,
@@ -131,25 +131,22 @@
       totalDeltas: new Float64Array(numRows),
     };
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'lastId': NUM,
-          'minValue': NUM,
-          'maxValue': NUM,
-          'lastValue': NUM,
-          'totalDelta': NUM,
-        },
-        rawResult);
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = fromNs(it.row.tsq);
-      data.lastIds[i] = it.row.lastId;
-      data.minValues[i] = it.row.minValue;
-      data.maxValues[i] = it.row.maxValue;
-      data.lastValues[i] = it.row.lastValue;
-      data.totalDeltas[i] = it.row.totalDelta;
+    const it = queryRes.iter({
+      'tsq': NUM,
+      'lastId': NUM,
+      'minValue': NUM,
+      'maxValue': NUM,
+      'lastValue': NUM,
+      'totalDelta': NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.timestamps[row] = fromNs(it.tsq);
+      data.lastIds[row] = it.lastId;
+      data.minValues[row] = it.minValue;
+      data.maxValues[row] = it.maxValue;
+      data.lastValues[row] = it.lastValue;
+      data.totalDeltas[row] = it.totalDelta;
     }
-
     return data;
   }
 
diff --git a/ui/src/tracks/cpu_freq/controller.ts b/ui/src/tracks/cpu_freq/controller.ts
index 08ca0b5..c457da7 100644
--- a/ui/src/tracks/cpu_freq/controller.ts
+++ b/ui/src/tracks/cpu_freq/controller.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {RawQueryResult} from '../../common/protos';
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL} from '../../common/query_iterator';
+import {QueryResult} from '../../common/query_result';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -41,30 +41,37 @@
     this.maximumValueSeen = await this.queryMaxFrequency();
     this.maxDurNs = await this.queryMaxSourceDur();
 
-    const result = await this.query(`
-      select max(ts), dur, count(1)
+    const iter = (await this.queryV2(`
+      select max(ts) as maxTs, dur, count(1) as rowCount
       from ${this.tableName('freq_idle')}
-    `);
-    this.maxTsEndNs =
-        result.columns[0].longValues![0] + result.columns[1].longValues![0];
+    `)).firstRow({maxTs: NUM_NULL, dur: NUM_NULL, rowCount: NUM});
+    if (iter.maxTs === null || iter.dur === null) {
+      // We shoulnd't really hit this because trackDecider shouldn't create
+      // the track in the first place if there are no entries. But could happen
+      // if only one cpu has no cpufreq data.
+      return;
+    }
+    this.maxTsEndNs = iter.maxTs + iter.dur;
 
-    const rowCount = result.columns[2].longValues![0];
+    const rowCount = iter.rowCount;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+
+    await this.queryV2(`
       create table ${this.tableName('freq_idle_cached')} as
       select
-        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
-        min(freq_value) as min_freq,
-        max(freq_value) as max_freq,
-        value_at_max_ts(ts, freq_value) as last_freq,
-        value_at_max_ts(ts, idle_value) as last_idle_value
+        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cachedTsq,
+        min(freqValue) as minFreq,
+        max(freqValue) as maxFreq,
+        value_at_max_ts(ts, freqValue) as lastFreq,
+        value_at_max_ts(ts, idleValue) as lastIdleValue
       from ${this.tableName('freq_idle')}
-      group by cached_tsq
-      order by cached_tsq
+      group by cachedTsq
+      order by cachedTsq
     `);
+
     this.cachedBucketNs = bucketNs;
   }
 
@@ -82,10 +89,10 @@
     // be an even number, so we can snap in the middle.
     const bucketNs =
         Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
-
     const freqResult = await this.queryData(startNs, endNs, bucketNs);
+    assertTrue(freqResult.isComplete());
 
-    const numRows = slowlyCountRows(freqResult);
+    const numRows = freqResult.numRows();
     const data: Data = {
       start,
       end,
@@ -100,68 +107,68 @@
       lastIdleValues: new Int8Array(numRows),
     };
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'minFreq': NUM,
-          'maxFreq': NUM,
-          'lastFreq': NUM,
-          'lastIdleValue': NUM,
-        },
-        freqResult);
+    const it = freqResult.iter({
+      'tsq': NUM,
+      'minFreq': NUM,
+      'maxFreq': NUM,
+      'lastFreq': NUM,
+      'lastIdleValue': NUM,
+    });
     for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = fromNs(it.row.tsq);
-      data.minFreqKHz[i] = it.row.minFreq;
-      data.maxFreqKHz[i] = it.row.maxFreq;
-      data.lastFreqKHz[i] = it.row.lastFreq;
-      data.lastIdleValues[i] = it.row.lastIdleValue;
+      data.timestamps[i] = fromNs(it.tsq);
+      data.minFreqKHz[i] = it.minFreq;
+      data.maxFreqKHz[i] = it.maxFreq;
+      data.lastFreqKHz[i] = it.lastFreq;
+      data.lastIdleValues[i] = it.lastIdleValue;
     }
+
     return data;
   }
 
   private async queryData(startNs: number, endNs: number, bucketNs: number):
-      Promise<RawQueryResult> {
+      Promise<QueryResult> {
     const isCached = this.cachedBucketNs <= bucketNs;
 
     if (isCached) {
-      return this.query(`
+      return this.queryV2(`
         select
-          cached_tsq / ${bucketNs} * ${bucketNs} as tsq,
-          min(min_freq) as minFreq,
-          max(max_freq) as maxFreq,
-          value_at_max_ts(cached_tsq, last_freq) as lastFreq,
-          value_at_max_ts(cached_tsq, last_idle_value) as lastIdleValue
+          cachedTsq / ${bucketNs} * ${bucketNs} as tsq,
+          min(minFreq) as minFreq,
+          max(maxFreq) as maxFreq,
+          value_at_max_ts(cachedTsq, lastFreq) as lastFreq,
+          value_at_max_ts(cachedTsq, lastIdleValue) as lastIdleValue
         from ${this.tableName('freq_idle_cached')}
         where
-          cached_tsq >= ${startNs - this.maxDurNs} and
-          cached_tsq <= ${endNs}
+          cachedTsq >= ${startNs - this.maxDurNs} and
+          cachedTsq <= ${endNs}
         group by tsq
         order by tsq
       `);
     }
-
-    const minTsFreq = await this.query(`
-      select ifnull(max(ts), 0) from ${this.tableName('freq')}
+    const minTsFreq = await this.queryV2(`
+      select ifnull(max(ts), 0) as minTs from ${this.tableName('freq')}
       where ts < ${startNs}
     `);
-    let minTs = minTsFreq.columns[0].longValues![0];
+
+    let minTs = minTsFreq.iter({minTs: NUM}).minTs;
     if (this.config.idleTrackId !== undefined) {
-      const minTsIdle = await this.query(`
-        select ifnull(max(ts), 0) from ${this.tableName('idle')}
+      const minTsIdle = await this.queryV2(`
+        select ifnull(max(ts), 0) as minTs from ${this.tableName('idle')}
         where ts < ${startNs}
       `);
-      minTs = Math.min(minTsIdle.columns[0].longValues![0], minTs);
+      minTs = Math.min(minTsIdle.iter({minTs: NUM}).minTs, minTs);
     }
+
     const geqConstraint = this.config.idleTrackId === undefined ?
         `ts >= ${minTs}` :
         `source_geq(ts, ${minTs})`;
-    return this.query(`
+    return this.queryV2(`
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
-        min(freq_value) as minFreq,
-        max(freq_value) as maxFreq,
-        value_at_max_ts(ts, freq_value) as lastFreq,
-        value_at_max_ts(ts, idle_value) as lastIdleValue
+        min(freqValue) as minFreq,
+        max(freqValue) as maxFreq,
+        value_at_max_ts(ts, freqValue) as lastFreq,
+        value_at_max_ts(ts, idleValue) as lastIdleValue
       from ${this.tableName('freq_idle')}
       where
         ${geqConstraint} and
@@ -172,59 +179,59 @@
   }
 
   private async queryMaxFrequency(): Promise<number> {
-    const result = await this.query(`
-      select max(freq_value)
+    const result = await this.queryV2(`
+      select max(freqValue) as maxFreq
       from ${this.tableName('freq')}
     `);
-    return result.columns[0].doubleValues![0];
+    return result.firstRow({'maxFreq': NUM_NULL}).maxFreq || 0;
   }
 
   private async queryMaxSourceDur(): Promise<number> {
-    const maxDurFreqResult =
-        await this.query(`select max(dur) from ${this.tableName('freq')}`);
-    const maxFreqDurNs = maxDurFreqResult.columns[0].longValues![0];
+    const maxDurFreqResult = await this.queryV2(
+        `select ifnull(max(dur), 0) as maxDur from ${this.tableName('freq')}`);
+    const maxDurNs = maxDurFreqResult.firstRow({'maxDur': NUM}).maxDur;
     if (this.config.idleTrackId === undefined) {
-      return maxFreqDurNs;
+      return maxDurNs;
     }
 
-    const maxDurIdleResult =
-        await this.query(`select max(dur) from ${this.tableName('idle')}`);
-    return Math.max(maxFreqDurNs, maxDurIdleResult.columns[0].longValues![0]);
+    const maxDurIdleResult = await this.queryV2(
+        `select ifnull(max(dur), 0) as maxDur from ${this.tableName('idle')}`);
+    return Math.max(maxDurNs, maxDurIdleResult.firstRow({maxDur: NUM}).maxDur);
   }
 
   private async createFreqIdleViews() {
-    await this.query(`create view ${this.tableName('freq')} as
+    await this.queryV2(`create view ${this.tableName('freq')} as
       select
         ts,
         dur,
-        value as freq_value
+        value as freqValue
       from experimental_counter_dur c
       where track_id = ${this.config.freqTrackId};
     `);
 
     if (this.config.idleTrackId === undefined) {
-      await this.query(`create view ${this.tableName('freq_idle')} as
+      await this.queryV2(`create view ${this.tableName('freq_idle')} as
         select
           ts,
           dur,
-          -1 as idle_value,
-          freq_value
+          -1 as idleValue,
+          freqValue
         from ${this.tableName('freq')};
       `);
       return;
     }
 
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('idle')} as
       select
         ts,
         dur,
-        iif(value = 4294967295, -1, cast(value as int)) as idle_value
+        iif(value = 4294967295, -1, cast(value as int)) as idleValue
       from experimental_counter_dur c
       where track_id = ${this.config.idleTrackId};
     `);
 
-    await this.query(`
+    await this.queryV2(`
       create virtual table ${this.tableName('freq_idle')}
       using span_join(${this.tableName('freq')}, ${this.tableName('idle')});
     `);
diff --git a/ui/src/tracks/cpu_profile/controller.ts b/ui/src/tracks/cpu_profile/controller.ts
index 0042888..a6c5456 100644
--- a/ui/src/tracks/cpu_profile/controller.ts
+++ b/ui/src/tracks/cpu_profile/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {
   TrackController,
   trackControllerRegistry
@@ -36,9 +36,8 @@
       where utid = ${this.config.utid}
       order by ts`;
 
-    const result = await this.query(query);
-
-    const numRows = slowlyCountRows(result);
+    const result = await this.queryV2(query);
+    const numRows = result.numRows();
     const data: Data = {
       start,
       end,
@@ -49,11 +48,11 @@
       callsiteId: new Uint32Array(numRows),
     };
 
-    const it = iter({id: NUM, ts: NUM, callsiteId: NUM}, result);
-    for (let i = 0; it.valid(); it.next(), ++i) {
-      data.ids[i] = it.row.id;
-      data.tsStarts[i] = it.row.ts;
-      data.callsiteId[i] = it.row.callsiteId;
+    const it = result.iter({id: NUM, ts: NUM, callsiteId: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      data.ids[row] = it.id;
+      data.tsStarts[row] = it.ts;
+      data.callsiteId[row] = it.callsiteId;
     }
 
     return data;
diff --git a/ui/src/tracks/cpu_slices/controller.ts b/ui/src/tracks/cpu_slices/controller.ts
index d6ac723..2d891ef 100644
--- a/ui/src/tracks/cpu_slices/controller.ts
+++ b/ui/src/tracks/cpu_slices/controller.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -29,7 +29,7 @@
   private maxDurNs = 0;
 
   async onSetup() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('sched')} as
       select
         ts,
@@ -40,18 +40,18 @@
       where cpu = ${this.config.cpu} and utid != 0
     `);
 
-    const rawResult = await this.query(`
-      select max(dur), count(1)
+    const queryRes = await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur, count(1) as rowCount
       from ${this.tableName('sched')}
     `);
-    this.maxDurNs = rawResult.columns[0].longValues![0];
-
-    const rowCount = rawResult.columns[1].longValues![0];
+    const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
+    this.maxDurNs = row.maxDur;
+    const rowCount = row.rowCount;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+    await this.queryV2(`
       create table ${this.tableName('sched_cached')} as
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
@@ -90,7 +90,7 @@
         isCached ? this.tableName('sched_cached') : this.tableName('sched');
     const constraintColumn = isCached ? 'cached_tsq' : 'ts';
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
         ${queryTsq} as tsq,
         ts,
@@ -105,7 +105,7 @@
       order by tsq
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -117,31 +117,30 @@
       utids: new Uint32Array(numRows),
     };
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({tsq: NUM, ts: NUM, dur: NUM, utid: NUM, id: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.utids[row] = +cols[3].longValues![row];
-      slices.ids[row] = +cols[4].longValues![row];
+      slices.utids[row] = it.utid;
+      slices.ids[row] = it.id;
     }
 
     return slices;
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('sched_cached')}`);
+    await this.queryV2(
+        `drop table if exists ${this.tableName('sched_cached')}`);
   }
 }
 
diff --git a/ui/src/tracks/debug_slices/controller.ts b/ui/src/tracks/debug_slices/controller.ts
index 0afcb55..8400af8 100644
--- a/ui/src/tracks/debug_slices/controller.ts
+++ b/ui/src/tracks/debug_slices/controller.ts
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertTrue} from '../../base/logging';
 import {Actions} from '../../common/actions';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {globals} from '../../controller/globals';
 import {
@@ -28,28 +27,26 @@
   static readonly kind = DEBUG_SLICE_TRACK_KIND;
 
   async onReload() {
-    const rawResult = await this.query(`select max(depth) from debug_slices`);
-    const maxDepth = (slowlyCountRows(rawResult) === 0) ?
-        1 :
-        rawResult.columns[0].longValues![0];
+    const rawResult = await this.queryV2(
+        `select ifnull(max(depth), 1) as maxDepth from debug_slices`);
+    const maxDepth = rawResult.firstRow({maxDepth: NUM}).maxDepth;
     globals.dispatch(
         Actions.updateTrackConfig({id: this.trackId, config: {maxDepth}}));
   }
 
   async onBoundsChange(start: number, end: number, resolution: number):
       Promise<Data> {
-    const rawResult = await this.query(`select id, name, ts,
-        iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur),
-        depth from debug_slices where
-        (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
+    const queryRes = await this.queryV2(`select
+      ifnull(id, -1) as id,
+      ifnull(name, '[null]') as name,
+      ts,
+      iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur) as dur,
+      ifnull(depth, 0) as depth
+      from debug_slices
+      where (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
 
-    assertTrue(rawResult.columns.length === 5);
-    const [idCol, nameCol, tsCol, durCol, depthCol] = rawResult.columns;
-    const idValues = idCol.longValues! || idCol.doubleValues!;
-    const tsValues = tsCol.longValues! || tsCol.doubleValues!;
-    const durValues = durCol.longValues! || durCol.doubleValues!;
+    const numRows = queryRes.numRows();
 
-    const numRows = slowlyCountRows(rawResult);
     const slices: Data = {
       start,
       end,
@@ -75,24 +72,24 @@
       return idx;
     }
 
-    for (let i = 0; i < slowlyCountRows(rawResult); i++) {
+    const it = queryRes.iter(
+        {id: NUM, name: STR, ts: NUM_NULL, dur: NUM_NULL, depth: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
       let sliceStart: number, sliceEnd: number;
-      if (tsCol.isNulls![i] || durCol.isNulls![i]) {
+      if (it.ts === null || it.dur === null) {
         sliceStart = sliceEnd = -1;
       } else {
-        sliceStart = tsValues[i];
-        const sliceDur = durValues[i];
-        sliceEnd = sliceStart + sliceDur;
+        sliceStart = it.ts;
+        sliceEnd = sliceStart + it.dur;
       }
-      slices.sliceIds[i] = idCol.isNulls![i] ? -1 : idValues[i];
-      slices.starts[i] = fromNs(sliceStart);
-      slices.ends[i] = fromNs(sliceEnd);
-      slices.depths[i] = depthCol.isNulls![i] ? 0 : depthCol.longValues![i];
-      const sliceName =
-          nameCol.isNulls![i] ? '[null]' : nameCol.stringValues![i];
-      slices.titles[i] = internString(sliceName);
-      slices.isInstant[i] = 0;
-      slices.isIncomplete[i] = 0;
+      slices.sliceIds[row] = it.id;
+      slices.starts[row] = fromNs(sliceStart);
+      slices.ends[row] = fromNs(sliceEnd);
+      slices.depths[row] = it.depth;
+      const sliceName = it.name;
+      slices.titles[row] = internString(sliceName);
+      slices.isInstant[row] = 0;
+      slices.isIncomplete[row] = 0;
     }
 
     return slices;
diff --git a/ui/src/tracks/expected_frames/controller.ts b/ui/src/tracks/expected_frames/controller.ts
index 9707059..5227a81 100644
--- a/ui/src/tracks/expected_frames/controller.ts
+++ b/ui/src/tracks/expected_frames/controller.ts
@@ -12,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../../base/logging';
-import {
-  iter,
-  NUM,
-  singleRow,
-  slowlyCountRows,
-  STR
-} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -44,17 +38,16 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
         select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
           as maxDur
         from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      const row = singleRow({maxDur: NUM}, maxDurResult);
-      this.maxDurNs = assertExists(row).maxDur;
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
@@ -73,7 +66,7 @@
       order by tsq, layout_depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -101,39 +94,35 @@
     }
     const greenIndex = internString('#4CAF50');
 
-    const it = iter(
-        {
-          tsq: NUM,
-          ts: NUM,
-          dur: NUM,
-          layoutDepth: NUM,
-          id: NUM,
-          name: STR,
-          isInstant: NUM,
-          isIncomplete: NUM,
-        },
-        rawResult);
-    for (let i = 0; it.valid(); it.next(), ++i) {
-      const startNsQ = it.row.tsq;
-      const startNs = it.row.ts;
-      const durNs = it.row.dur;
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      layoutDepth: NUM,
+      id: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
-      slices.starts[i] = fromNs(startNsQ);
-      slices.ends[i] = fromNs(endNsQ);
-      slices.depths[i] = it.row.layoutDepth;
-      slices.titles[i] = internString(it.row.name);
-      slices.sliceIds[i] = it.row.id;
-      slices.isInstant[i] = it.row.isInstant;
-      slices.isIncomplete[i] = it.row.isIncomplete;
-      slices.colors![i] = greenIndex;
+      slices.starts[row] = fromNs(startNsQ);
+      slices.ends[row] = fromNs(endNsQ);
+      slices.depths[row] = it.layoutDepth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
+      slices.colors![row] = greenIndex;
     }
     return slices;
   }
diff --git a/ui/src/tracks/heap_profile/controller.ts b/ui/src/tracks/heap_profile/controller.ts
index 2422af1..9873744 100644
--- a/ui/src/tracks/heap_profile/controller.ts
+++ b/ui/src/tracks/heap_profile/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, STR} from '../../common/query_iterator';
 import {
   TrackController,
   trackControllerRegistry
@@ -38,7 +38,7 @@
         types: new Array<string>()
       };
     }
-    const result = await this.query(`
+    const queryRes = await this.queryV2(`
     select * from
     (select distinct(ts) as ts, 'native' as type from heap_profile_allocation
      where upid = ${this.config.upid}
@@ -46,7 +46,7 @@
         select distinct(graph_sample_ts) as ts, 'graph' as type from
         heap_graph_object
         where upid = ${this.config.upid}) order by ts`);
-    const numRows = slowlyCountRows(result);
+    const numRows = queryRes.numRows();
     const data: Data = {
       start,
       end,
@@ -56,11 +56,11 @@
       types: new Array<string>(numRows),
     };
 
-    for (let row = 0; row < numRows; row++) {
-      data.tsStarts[row] = +result.columns[0].longValues![row];
-      data.types[row] = result.columns[1].stringValues![row];
+    const it = queryRes.iter({ts: NUM, type: STR});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.tsStarts[row] = it.ts;
+      data.types[row] = it.type;
     }
-
     return data;
   }
 }
diff --git a/ui/src/tracks/process_scheduling/controller.ts b/ui/src/tracks/process_scheduling/controller.ts
index 687d16d..9441448 100644
--- a/ui/src/tracks/process_scheduling/controller.ts
+++ b/ui/src/tracks/process_scheduling/controller.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {RawQueryResult} from '../../common/protos';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
+import {QueryResult} from '../../common/query_result';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -45,18 +45,19 @@
     assertTrue(cpus.length > 0);
     this.maxCpu = Math.max(...cpus) + 1;
 
-    const result = await this.query(`
-      select max(dur), count(1)
+    const result = (await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur, count(1) as count
       from ${this.tableName('process_sched')}
-    `);
-    this.maxDurNs = result.columns[0].longValues![0];
+    `)).iter({maxDur: NUM, count: NUM});
+    assertTrue(result.valid());
+    this.maxDurNs = result.maxDur;
 
-    const rowCount = result.columns[1].longValues![0];
+    const rowCount = result.count;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+    await this.queryV2(`
       create table ${this.tableName('process_sched_cached')} as
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
@@ -88,9 +89,8 @@
     const bucketNs =
         Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
 
-    const rawResult = await this.queryData(startNs, endNs, bucketNs);
-
-    const numRows = slowlyCountRows(rawResult);
+    const queryRes = await this.queryData(startNs, endNs, bucketNs);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       kind: 'slice',
       start,
@@ -104,38 +104,43 @@
       utids: new Uint32Array(numRows),
     };
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      cpu: NUM,
+      utid: NUM,
+    });
+
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.cpus[row] = +cols[3].longValues![row];
-      slices.utids[row] = +cols[4].longValues![row];
+      slices.cpus[row] = it.cpu;
+      slices.utids[row] = it.utid;
       slices.end = Math.max(slices.ends[row], slices.end);
     }
     return slices;
   }
 
   private queryData(startNs: number, endNs: number, bucketNs: number):
-      Promise<RawQueryResult> {
+      Promise<QueryResult> {
     const isCached = this.cachedBucketNs <= bucketNs;
     const tsq = isCached ? `cached_tsq / ${bucketNs} * ${bucketNs}` :
                            `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
     const queryTable = isCached ? this.tableName('process_sched_cached') :
                                   this.tableName('process_sched');
     const constraintColumn = isCached ? 'cached_tsq' : 'ts';
-    return this.query(`
+    return this.queryV2(`
       select
         ${tsq} as tsq,
         ts,
@@ -152,7 +157,7 @@
   }
 
   private async createSchedView() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('process_sched')} as
       select ts, dur, cpu, utid
       from experimental_sched_upid
diff --git a/ui/src/tracks/process_summary/controller.ts b/ui/src/tracks/process_summary/controller.ts
index 75242fe..9e6ec5e 100644
--- a/ui/src/tracks/process_summary/controller.ts
+++ b/ui/src/tracks/process_summary/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {LIMIT} from '../../common/track_data';
 import {
@@ -39,29 +39,35 @@
     const endNs = toNs(end);
 
     if (this.setup === false) {
-      await this.query(
+      await this.queryV2(
           `create virtual table ${this.tableName('window')} using window;`);
 
       let utids = [this.config.utid];
       if (this.config.upid) {
-        const threadQuery = await this.query(
+        const threadQuery = await this.queryV2(
             `select utid from thread where upid=${this.config.upid}`);
-        utids = threadQuery.columns[0].longValues!;
+        utids = [];
+        for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
+          utids.push(it.utid);
+        }
       }
 
-      const trackQuery = await this.query(
+      const trackQuery = await this.queryV2(
           `select id from thread_track where utid in (${utids.join(',')})`);
-      const tracks = trackQuery.columns[0].longValues!;
+      const tracks = [];
+      for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
+        tracks.push(it.id);
+      }
 
       const processSliceView = this.tableName('process_slice_view');
-      await this.query(
+      await this.queryV2(
           `create view ${processSliceView} as ` +
           // 0 as cpu is a dummy column to perform span join on.
           `select ts, dur/${utids.length} as dur ` +
           `from slice s ` +
           `where depth = 0 and track_id in ` +
           `(${tracks.join(',')})`);
-      await this.query(`create virtual table ${this.tableName('span')}
+      await this.queryV2(`create virtual table ${this.tableName('span')}
           using span_join(${processSliceView},
                           ${this.tableName('window')});`);
       this.setup = true;
@@ -73,7 +79,7 @@
     const windowStartNs = Math.floor(startNs / bucketSizeNs) * bucketSizeNs;
     const windowDurNs = Math.max(1, endNs - windowStartNs);
 
-    this.query(`update ${this.tableName('window')} set
+    await this.queryV2(`update ${this.tableName('window')} set
       window_start=${windowStartNs},
       window_dur=${windowDurNs},
       quantum=${bucketSizeNs}
@@ -98,9 +104,6 @@
       group by quantum_ts
       limit ${LIMIT}`;
 
-    const rawResult = await this.query(query);
-    const numRows = slowlyCountRows(rawResult);
-
     const summary: Data = {
       start,
       end,
@@ -109,21 +112,24 @@
       bucketSizeSeconds: fromNs(bucketSizeNs),
       utilizations: new Float64Array(numBuckets),
     };
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const bucket = +cols[0].longValues![row];
+
+    const queryRes = await this.queryV2(query);
+    const it = queryRes.iter({bucket: NUM, utilization: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
       if (bucket > numBuckets) {
         continue;
       }
-      summary.utilizations[bucket] = +cols[1].doubleValues![row];
+      summary.utilizations[bucket] = it.utilization;
     }
+
     return summary;
   }
 
   onDestroy(): void {
     if (this.setup) {
-      this.query(`drop table ${this.tableName('window')}`);
-      this.query(`drop table ${this.tableName('span')}`);
+      this.queryV2(`drop table ${this.tableName('window')}`);
+      this.queryV2(`drop table ${this.tableName('span')}`);
       this.setup = false;
     }
   }
diff --git a/ui/src/tracks/thread_state/controller.ts b/ui/src/tracks/thread_state/controller.ts
index a4718c6..0de3a3e 100644
--- a/ui/src/tracks/thread_state/controller.ts
+++ b/ui/src/tracks/thread_state/controller.ts
@@ -14,10 +14,8 @@
 
 import {assertFalse} from '../../base/logging';
 import {
-  iter,
   NUM,
   NUM_NULL,
-  slowlyCountRows,
   STR_NULL
 } from '../../common/query_iterator';
 import {translateState} from '../../common/thread_state';
@@ -39,7 +37,7 @@
   private maxDurNs = 0;
 
   async onSetup() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('thread_state')} as
       select
         id,
@@ -52,11 +50,11 @@
       where utid = ${this.config.utid} and utid != 0
     `);
 
-    const rawResult = await this.query(`
-      select max(dur)
+    const queryRes = await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur
       from ${this.tableName('thread_state')}
     `);
-    this.maxDurNs = rawResult.columns[0].longValues![0];
+    this.maxDurNs = queryRes.firstRow({maxDur: NUM}).maxDur;
   }
 
   async onBoundsChange(start: number, end: number, resolution: number):
@@ -75,10 +73,10 @@
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
         max(dur) as dur,
-        cast(cpu as integer) as cpu,
+        ifnull(cast(cpu as integer), -1) as cpu,
         state,
         io_wait,
-        id
+        ifnull(id, -1) as id
       from ${this.tableName('thread_state')}
       where
         ts >= ${startNs - this.maxDurNs} and
@@ -87,8 +85,8 @@
       order by tsq, state, io_wait
     `;
 
-    const result = await this.query(query);
-    const numRows = slowlyCountRows(result);
+    const queryRes = await this.queryV2(query);
+    const numRows = queryRes.numRows();
 
     const data: Data = {
       start,
@@ -113,31 +111,28 @@
       stringIndexes.set({shortState, ioWait}, idx);
       return idx;
     }
-    iter(
-        {
-          'ts': NUM,
-          'dur': NUM,
-          'cpu': NUM_NULL,
-          'state': STR_NULL,
-          'io_wait': NUM_NULL,
-          'id': NUM_NULL,
-        },
-        result);
-    for (let row = 0; row < numRows; row++) {
-      const cols = result.columns;
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      'tsq': NUM,
+      'ts': NUM,
+      'dur': NUM,
+      'cpu': NUM,
+      'state': STR_NULL,
+      'io_wait': NUM_NULL,
+      'id': NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      const cpu = cols[3].isNulls![row] ? -1 : cols[3].longValues![row];
-      const state = cols[4].stringValues![row];
-      const ioWait =
-          cols[5].isNulls![row] ? undefined : !!cols[5].longValues![row];
-      const id = cols[6].isNulls![row] ? -1 : cols[6].longValues![row];
+      const cpu = it.cpu;
+      const state = it.state || '[null]';
+      const ioWait = it.io_wait === null ? undefined : !!it.io_wait;
+      const id = it.id;
 
       // We should never have the end timestamp being the same as the bucket
       // start.
@@ -153,7 +148,8 @@
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('thread_state')}`);
+    await this.queryV2(
+        `drop table if exists ${this.tableName('thread_state')}`);
   }
 }