blob: 8e1fa816fd73f570337f31740b39a6e6be705670 [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {assertTrue} from '../base/logging';
import {RawQueryResult} from './protos';
// These types are used both for the new streaming query iterator and the old
// columnar RawQueryResult.
export const NUM = 0;
export const STR = 'str';
export const NUM_NULL: number|null = 1;
export const STR_NULL: string|null = 'str_null';
export type ColumnType = string|number|null;
// One row extracted from an SQL result:
export interface Row {
[key: string]: ColumnType;
}
// The methods that any iterator has to implement.
export interface RowIteratorBase {
valid(): boolean;
next(): void;
}
// A RowIterator is a type that has all the fields defined in the query spec
// plus the valid() and next() operators. This is to ultimately allow the
// clients to do:
// const result = await engine.queryV2("select name, surname, id from people;");
// const iter = queryResult.iter({name: STR, surname: STR, id: NUM});
// for (; iter.valid(); iter.next())
// console.log(iter.name, iter.surname);
export type RowIterator<T extends Row> = RowIteratorBase&T;
// The old iterator for non-batched queries. Going away. Usage.
// const result = await engine.query("select 42 as n;");
// const it = getRowIterator({"answer": NUM}, result);
// for (; it.valid(); it.next()) {
// console.log(it.row.answer);
// }
export interface LegacyRowIterator<T extends Row> {
valid(): boolean;
next(): void;
row: T;
}
export function columnTypeToString(t: ColumnType): string {
switch (t) {
case NUM:
return 'NUM';
case NUM_NULL:
return 'NUM_NULL';
case STR:
return 'STR';
case STR_NULL:
return 'STR_NULL';
default:
return `INVALID(${t})`;
}
}
// TODO(primiano): the types and helpers in the rest of this file are
// transitional and will be removed once we migrate everything to the streaming
// query API.
// Exported for testing
export function findColumnIndex(
result: RawQueryResult, name: string, columnType: ColumnType): number {
let matchingDescriptorIndex = -1;
const disallowNulls = columnType === STR || columnType === NUM;
const expectsStrings = columnType === STR || columnType === STR_NULL;
const expectsNumbers = columnType === NUM || columnType === NUM_NULL;
const isEmpty = +result.numRecords === 0;
for (let i = 0; i < result.columnDescriptors.length; ++i) {
const descriptor = result.columnDescriptors[i];
const column = result.columns[i];
if (descriptor.name !== name) {
continue;
}
const hasDoubles = column.doubleValues && column.doubleValues.length;
const hasLongs = column.longValues && column.longValues.length;
const hasStrings = column.stringValues && column.stringValues.length;
if (matchingDescriptorIndex !== -1) {
throw new Error(`Multiple columns with the name ${name}`);
}
if (expectsStrings && !hasStrings && !isEmpty) {
throw new Error(`Expected strings for column ${name} but found numbers`);
}
if (expectsNumbers && !hasDoubles && !hasLongs && !isEmpty) {
throw new Error(`Expected numbers for column ${name} but found strings`);
}
if (disallowNulls) {
for (let j = 0; j < +result.numRecords; ++j) {
if (column.isNulls![j] === true) {
throw new Error(`Column ${name} contains nulls`);
}
}
}
matchingDescriptorIndex = i;
}
if (matchingDescriptorIndex === -1) {
throw new Error(`No column with name ${name} found in result.`);
}
return matchingDescriptorIndex;
}
class ColumnarRowIterator {
row: Row;
private i_: number;
private rowCount_: number;
private columnCount_: number;
private columnNames_: string[];
private columns_: Array<number[]|string[]>;
private nullColumns_: boolean[][];
constructor(querySpec: Row, queryResult: RawQueryResult) {
const row: Row = querySpec;
this.row = row;
this.i_ = 0;
this.rowCount_ = +queryResult.numRecords;
this.columnCount_ = 0;
this.columnNames_ = [];
this.columns_ = [];
this.nullColumns_ = [];
for (const [columnName, columnType] of Object.entries(querySpec)) {
const index = findColumnIndex(queryResult, columnName, columnType);
const column = queryResult.columns[index];
this.columnCount_++;
this.columnNames_.push(columnName);
let values: string[]|Array<number|Long> = [];
const isNum = columnType === NUM || columnType === NUM_NULL;
const isString = columnType === STR || columnType === STR_NULL;
if (isNum && column.longValues &&
column.longValues.length === this.rowCount_) {
values = column.longValues;
}
if (isNum && column.doubleValues &&
column.doubleValues.length === this.rowCount_) {
values = column.doubleValues;
}
if (isString && column.stringValues &&
column.stringValues.length === this.rowCount_) {
values = column.stringValues;
}
this.columns_.push(values as string[]);
this.nullColumns_.push(column.isNulls!);
}
if (this.rowCount_ > 0) {
for (let j = 0; j < this.columnCount_; ++j) {
const name = this.columnNames_[j];
const isNull = this.nullColumns_[j][this.i_];
this.row[name] = isNull ? null : this.columns_[j][this.i_];
}
}
}
valid(): boolean {
return this.i_ < this.rowCount_;
}
next(): void {
this.i_++;
for (let j = 0; j < this.columnCount_; ++j) {
const name = this.columnNames_[j];
const isNull = this.nullColumns_[j][this.i_];
this.row[name] = isNull ? null : this.columns_[j][this.i_];
}
}
}
// Deliberately not exported, use iter() below to make code easy to switch
// to other queryResult formats.
function iterFromColumns<T extends Row>(
querySpec: T, queryResult: RawQueryResult): LegacyRowIterator<T> {
const iter = new ColumnarRowIterator(querySpec, queryResult);
return iter as unknown as LegacyRowIterator<T>;
}
// Deliberately not exported, use iterUntyped() below to make code easy to
// switch to other queryResult formats.
function iterUntypedFromColumns(result: RawQueryResult):
LegacyRowIterator<Row> {
const spec: Row = {};
const desc = result.columnDescriptors;
for (let i = 0; i < desc.length; ++i) {
const name = desc[i].name;
if (!name) {
continue;
}
spec[name] = desc[i].type === 3 ? STR_NULL : NUM_NULL;
}
const iter = new ColumnarRowIterator(spec, result);
return iter as unknown as LegacyRowIterator<Row>;
}
export function iterUntyped(result: RawQueryResult): LegacyRowIterator<Row> {
return iterUntypedFromColumns(result);
}
export function iter<T extends Row>(
spec: T, result: RawQueryResult): LegacyRowIterator<T> {
return iterFromColumns(spec, result);
}
export function slowlyCountRows(result: RawQueryResult): number {
// This isn't actually slow for columnar data but it might be for other
// formats.
return +result.numRecords;
}
export function singleRow<T extends Row>(spec: T, result: RawQueryResult): T|
undefined {
const numRows = slowlyCountRows(result);
if (numRows === 0) {
return undefined;
}
if (numRows > 1) {
throw new Error(
`Attempted to extract single row but more than ${numRows} rows found.`);
}
const it = iter(spec, result);
assertTrue(it.valid());
return it.row;
}
export function singleRowUntyped(result: RawQueryResult): Row|undefined {
const numRows = slowlyCountRows(result);
if (numRows === 0) {
return undefined;
}
if (numRows > 1) {
throw new Error(
`Attempted to extract single row but more than ${numRows} rows found.`);
}
const it = iterUntyped(result);
assertTrue(it.valid());
return it.row;
}