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