1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {assertTrue} from '../base/logging'; 16 17import {RawQueryResult} from './protos'; 18 19// Union of all the query result formats that we can turn into forward 20// iterators. 21// TODO(hjd): Replace someOtherEncoding place holder with the real new 22// format. 23type QueryResult = RawQueryResult|{someOtherEncoding: string}; 24 25// One row extracted from an SQL result: 26interface Row { 27 [key: string]: string|number|null; 28} 29 30// API: 31// const result = await engine.query("select 42 as n;"); 32// const it = iter({"answer": NUM}, result); 33// for (; it.valid(); it.next()) { 34// console.log(it.row.answer); 35// } 36export interface RowIterator<T extends Row> { 37 valid(): boolean; 38 next(): void; 39 row: T; 40} 41 42export const NUM = 0; 43export const STR = 'str'; 44export const NUM_NULL: number|null = 1; 45export const STR_NULL: string|null = 'str_null'; 46export type ColumnType = 47 (typeof NUM)|(typeof STR)|(typeof NUM_NULL)|(typeof STR_NULL); 48 49// Exported for testing 50export function findColumnIndex( 51 result: RawQueryResult, name: string, columnType: number|null|string): 52 number { 53 let matchingDescriptorIndex = -1; 54 const disallowNulls = columnType === STR || columnType === NUM; 55 const expectsStrings = columnType === STR || columnType === STR_NULL; 56 const expectsNumbers = columnType === NUM || columnType === NUM_NULL; 57 const isEmpty = +result.numRecords === 0; 58 59 for (let i = 0; i < result.columnDescriptors.length; ++i) { 60 const descriptor = result.columnDescriptors[i]; 61 const column = result.columns[i]; 62 if (descriptor.name !== name) { 63 continue; 64 } 65 66 const hasDoubles = column.doubleValues && column.doubleValues.length; 67 const hasLongs = column.longValues && column.longValues.length; 68 const hasStrings = column.stringValues && column.stringValues.length; 69 70 if (matchingDescriptorIndex !== -1) { 71 throw new Error(`Multiple columns with the name ${name}`); 72 } 73 74 if (expectsStrings && !hasStrings && !isEmpty) { 75 throw new Error(`Expected strings for column ${name} but found numbers`); 76 } 77 78 if (expectsNumbers && !hasDoubles && !hasLongs && !isEmpty) { 79 throw new Error(`Expected numbers for column ${name} but found strings`); 80 } 81 82 if (disallowNulls) { 83 for (let j = 0; j < +result.numRecords; ++j) { 84 if (column.isNulls![j] === true) { 85 throw new Error(`Column ${name} contains nulls`); 86 } 87 } 88 } 89 matchingDescriptorIndex = i; 90 } 91 92 if (matchingDescriptorIndex === -1) { 93 throw new Error(`No column with name ${name} found in result.`); 94 } 95 96 return matchingDescriptorIndex; 97} 98 99class ColumnarRowIterator { 100 row: Row; 101 private i_: number; 102 private rowCount_: number; 103 private columnCount_: number; 104 private columnNames_: string[]; 105 private columns_: Array<number[]|string[]>; 106 private nullColumns_: boolean[][]; 107 108 constructor(querySpec: Row, queryResult: RawQueryResult) { 109 const row: Row = querySpec; 110 this.row = row; 111 this.i_ = 0; 112 this.rowCount_ = +queryResult.numRecords; 113 this.columnCount_ = 0; 114 this.columnNames_ = []; 115 this.columns_ = []; 116 this.nullColumns_ = []; 117 118 for (const [columnName, columnType] of Object.entries(querySpec)) { 119 const index = findColumnIndex(queryResult, columnName, columnType); 120 const column = queryResult.columns[index]; 121 this.columnCount_++; 122 this.columnNames_.push(columnName); 123 let values: string[]|Array<number|Long> = []; 124 const isNum = columnType === NUM || columnType === NUM_NULL; 125 const isString = columnType === STR || columnType === STR_NULL; 126 if (isNum && column.longValues && 127 column.longValues.length === this.rowCount_) { 128 values = column.longValues; 129 } 130 if (isNum && column.doubleValues && 131 column.doubleValues.length === this.rowCount_) { 132 values = column.doubleValues; 133 } 134 if (isString && column.stringValues && 135 column.stringValues.length === this.rowCount_) { 136 values = column.stringValues; 137 } 138 this.columns_.push(values as string[]); 139 this.nullColumns_.push(column.isNulls!); 140 } 141 if (this.rowCount_ > 0) { 142 for (let j = 0; j < this.columnCount_; ++j) { 143 const name = this.columnNames_[j]; 144 const isNull = this.nullColumns_[j][this.i_]; 145 this.row[name] = isNull ? null : this.columns_[j][this.i_]; 146 } 147 } 148 } 149 150 valid(): boolean { 151 return this.i_ < this.rowCount_; 152 } 153 154 next(): void { 155 this.i_++; 156 for (let j = 0; j < this.columnCount_; ++j) { 157 const name = this.columnNames_[j]; 158 const isNull = this.nullColumns_[j][this.i_]; 159 this.row[name] = isNull ? null : this.columns_[j][this.i_]; 160 } 161 } 162} 163 164// Deliberately not exported, use iter() below to make code easy to switch 165// to other queryResult formats. 166function iterFromColumns<T extends Row>( 167 querySpec: T, queryResult: RawQueryResult): RowIterator<T> { 168 const iter = new ColumnarRowIterator(querySpec, queryResult); 169 return iter as unknown as RowIterator<T>; 170} 171 172// Deliberately not exported, use iterUntyped() below to make code easy to 173// switch to other queryResult formats. 174function iterUntypedFromColumns(result: RawQueryResult): RowIterator<Row> { 175 const spec: Row = {}; 176 const desc = result.columnDescriptors; 177 for (let i = 0; i < desc.length; ++i) { 178 const name = desc[i].name; 179 if (!name) { 180 continue; 181 } 182 spec[name] = desc[i].type === 3 ? STR_NULL : NUM_NULL; 183 } 184 const iter = new ColumnarRowIterator(spec, result); 185 return iter as unknown as RowIterator<Row>; 186} 187 188function isColumnarQueryResult(result: QueryResult): result is RawQueryResult { 189 return (result as RawQueryResult).columnDescriptors !== undefined; 190} 191 192export function iterUntyped(result: QueryResult): RowIterator<Row> { 193 if (isColumnarQueryResult(result)) { 194 return iterUntypedFromColumns(result); 195 } else { 196 throw new Error('Unsuported format'); 197 } 198} 199 200export function iter<T extends Row>( 201 spec: T, result: QueryResult): RowIterator<T> { 202 if (isColumnarQueryResult(result)) { 203 return iterFromColumns(spec, result); 204 } else { 205 throw new Error('Unsuported format'); 206 } 207} 208 209export function slowlyCountRows(result: QueryResult): number { 210 if (isColumnarQueryResult(result)) { 211 // This isn't actually slow for columnar data but it might be for other 212 // formats. 213 return +result.numRecords; 214 } else { 215 throw new Error('Unsuported format'); 216 } 217} 218 219export function singleRow<T extends Row>(spec: T, result: QueryResult): T| 220 undefined { 221 const numRows = slowlyCountRows(result); 222 if (numRows === 0) { 223 return undefined; 224 } 225 if (numRows > 1) { 226 throw new Error( 227 `Attempted to extract single row but more than ${numRows} rows found.`); 228 } 229 const it = iter(spec, result); 230 assertTrue(it.valid()); 231 return it.row; 232} 233 234export function singleRowUntyped(result: QueryResult): Row|undefined { 235 const numRows = slowlyCountRows(result); 236 if (numRows === 0) { 237 return undefined; 238 } 239 if (numRows > 1) { 240 throw new Error( 241 `Attempted to extract single row but more than ${numRows} rows found.`); 242 } 243 const it = iterUntyped(result); 244 assertTrue(it.valid()); 245 return it.row; 246} 247