• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2021 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 {QueryResult as QueryResultProto} from '../protos';
16
17import {
18  createQueryResult,
19  decodeInt64Varint,
20  NUM,
21  NUM_NULL,
22  STR,
23  STR_NULL,
24} from './query_result';
25
26const T = QueryResultProto.CellsBatch.CellType;
27
28test('QueryResult.SimpleOneRow', () => {
29  const batch = QueryResultProto.CellsBatch.create({
30    cells: [T.CELL_STRING, T.CELL_VARINT, T.CELL_STRING, T.CELL_FLOAT64],
31    varintCells: [42],
32    stringCells: ['the foo', 'the bar'].join('\0'),
33    float64Cells: [42.42],
34    isLastBatch: true,
35  });
36  const resProto = QueryResultProto.create({
37    columnNames: ['a_str', 'b_int', 'c_str', 'd_float'],
38    batch: [batch],
39  });
40
41  const qr = createQueryResult({query: 'Some query'});
42  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
43  expect(qr.isComplete()).toBe(true);
44  expect(qr.numRows()).toBe(1);
45
46  // First try iterating without selecting any column.
47  {
48    const iter = qr.iter({});
49    expect(iter.valid()).toBe(true);
50    iter.next();
51    expect(iter.valid()).toBe(false);
52  }
53
54  // Then select only two of them.
55  {
56    const iter = qr.iter({c_str: STR, d_float: NUM});
57    expect(iter.valid()).toBe(true);
58    expect(iter.c_str).toBe('the bar');
59    expect(iter.d_float).toBeCloseTo(42.42);
60    iter.next();
61    expect(iter.valid()).toBe(false);
62  }
63
64  // If a column is not present in the result set, iter() should throw.
65  expect(() => qr.iter({nx: NUM})).toThrowError(/\bnx\b.*not found/);
66});
67
68test('QueryResult.BigNumbers', () => {
69  const numAndExpectedStr = [
70    [0, '0'],
71    [-1, '-1'],
72    [-1000, '-1000'],
73    [1e12, '1000000000000'],
74    [1e12 * -1, '-1000000000000'],
75    [((1 << 31) - 1) | 0, '2147483647'],
76    [1 << 31, '-2147483648'],
77    [Number.MAX_SAFE_INTEGER, '9007199254740991'],
78    [Number.MIN_SAFE_INTEGER, '-9007199254740991'],
79  ];
80  const batch = QueryResultProto.CellsBatch.create({
81    cells: new Array<number>(numAndExpectedStr.length).fill(T.CELL_VARINT),
82    varintCells: numAndExpectedStr.map((x) => x[0]) as number[],
83    isLastBatch: true,
84  });
85  const resProto = QueryResultProto.create({
86    columnNames: ['n'],
87    batch: [batch],
88  });
89
90  const qr = createQueryResult({query: 'Some query'});
91  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
92  const actual: string[] = [];
93  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
94    actual.push(BigInt(iter.n).toString());
95  }
96  expect(actual).toEqual(numAndExpectedStr.map((x) => x[1]) as string[]);
97});
98
99test('QueryResult.Floats', () => {
100  const floats = [
101    0.0,
102    1.0,
103    -1.0,
104    3.14159265358,
105    Number.MIN_SAFE_INTEGER,
106    Number.MAX_SAFE_INTEGER,
107    Number.NEGATIVE_INFINITY,
108    Number.POSITIVE_INFINITY,
109    Number.NaN,
110  ];
111  const batch = QueryResultProto.CellsBatch.create({
112    cells: new Array<number>(floats.length).fill(T.CELL_FLOAT64),
113    float64Cells: floats,
114    isLastBatch: true,
115  });
116  const resProto = QueryResultProto.create({
117    columnNames: ['n'],
118    batch: [batch],
119  });
120
121  const qr = createQueryResult({query: 'Some query'});
122  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
123  const actual: number[] = [];
124  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
125    actual.push(iter.n);
126  }
127  expect(actual).toEqual(floats);
128});
129
130test('QueryResult.Strings', () => {
131  const strings = [
132    'a',
133    '',
134    '',
135    'hello world',
136    'In einem Bächlein helle da schoß in froher Eil',
137    '色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず',
138  ];
139  const batch = QueryResultProto.CellsBatch.create({
140    cells: new Array<number>(strings.length).fill(T.CELL_STRING),
141    stringCells: strings.join('\0'),
142    isLastBatch: true,
143  });
144  const resProto = QueryResultProto.create({
145    columnNames: ['s'],
146    batch: [batch],
147  });
148
149  const qr = createQueryResult({query: 'Some query'});
150  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
151  const actual: string[] = [];
152  for (const iter = qr.iter({s: STR}); iter.valid(); iter.next()) {
153    actual.push(iter.s);
154  }
155  expect(actual).toEqual(strings);
156});
157
158test('QueryResult.NullChecks', () => {
159  const cells: number[] = [];
160  cells.push(T.CELL_VARINT, T.CELL_NULL);
161  cells.push(T.CELL_NULL, T.CELL_STRING);
162  cells.push(T.CELL_VARINT, T.CELL_STRING);
163  const batch = QueryResultProto.CellsBatch.create({
164    cells,
165    varintCells: [1, 2],
166    stringCells: ['a', 'b'].join('\0'),
167    isLastBatch: true,
168  });
169  const resProto = QueryResultProto.create({
170    columnNames: ['n', 's'],
171    batch: [batch],
172  });
173
174  const qr = createQueryResult({query: 'Some query'});
175  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
176  const actualNums = new Array<number | null>();
177  const actualStrings = new Array<string | null>();
178  for (
179    const iter = qr.iter({n: NUM_NULL, s: STR_NULL});
180    iter.valid();
181    iter.next()
182  ) {
183    actualNums.push(iter.n);
184    actualStrings.push(iter.s);
185  }
186  expect(actualNums).toEqual([1, null, 2]);
187  expect(actualStrings).toEqual([null, 'a', 'b']);
188
189  // Check that using NUM / STR throws.
190  expect(() => qr.iter({n: NUM_NULL, s: STR})).toThrowError(
191    /col: 's'.*is NULL.*not expected/,
192  );
193  expect(() => qr.iter({n: NUM, s: STR_NULL})).toThrowError(
194    /col: 'n'.*is NULL.*not expected/,
195  );
196  expect(qr.iter({n: NUM_NULL})).toBeTruthy();
197  expect(qr.iter({s: STR_NULL})).toBeTruthy();
198});
199
200test('QueryResult.EarlyError', () => {
201  const resProto = QueryResultProto.create({
202    columnNames: [],
203    batch: [{isLastBatch: true}],
204    error: 'Oh dear, this SQL query is too complicated, I give up',
205  });
206  const qr = createQueryResult({query: 'Some query'});
207  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
208  expect(qr.error()).toContain('Oh dear');
209  expect(qr.isComplete()).toBe(true);
210  const iter = qr.iter({});
211  expect(iter.valid()).toBe(false);
212});
213
214test('QueryResult.LateError', () => {
215  const resProto = QueryResultProto.create({
216    columnNames: ['n'],
217    batch: [
218      {
219        cells: [T.CELL_VARINT],
220        varintCells: [1],
221      },
222      {
223        cells: [T.CELL_VARINT],
224        varintCells: [2],
225        isLastBatch: true,
226      },
227    ],
228    error: 'I tried, I was getting there, but then I failed',
229  });
230  const qr = createQueryResult({query: 'Some query'});
231  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
232  expect(qr.error()).toContain('I failed');
233  const rows: number[] = [];
234  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
235    rows.push(iter.n);
236  }
237  expect(rows).toEqual([1, 2]);
238  expect(qr.isComplete()).toBe(true);
239});
240
241test('QueryResult.MultipleBatches', async () => {
242  const batch1 = QueryResultProto.create({
243    columnNames: ['n'],
244    batch: [
245      {
246        cells: [T.CELL_VARINT],
247        varintCells: [1],
248        isLastBatch: false,
249      },
250    ],
251  });
252  const batch2 = QueryResultProto.create({
253    batch: [
254      {
255        cells: [T.CELL_VARINT],
256        varintCells: [2],
257        isLastBatch: true,
258      },
259    ],
260  });
261
262  const qr = createQueryResult({query: 'Some query'});
263  expect(qr.isComplete()).toBe(false);
264
265  qr.appendResultBatch(QueryResultProto.encode(batch1).finish());
266  qr.appendResultBatch(QueryResultProto.encode(batch2).finish());
267
268  const awaitRes = await qr;
269
270  expect(awaitRes.isComplete()).toBe(true);
271  expect(qr.isComplete()).toBe(true);
272
273  expect(awaitRes.numRows()).toBe(2);
274  expect(qr.numRows()).toBe(2);
275});
276
277// Regression test for b/194891824 .
278test('QueryResult.DuplicateColumnNames', () => {
279  const batch = QueryResultProto.CellsBatch.create({
280    cells: [
281      T.CELL_VARINT,
282      T.CELL_STRING,
283      T.CELL_FLOAT64,
284      T.CELL_STRING,
285      T.CELL_STRING,
286    ],
287    varintCells: [42],
288    stringCells: ['a', 'b', 'c'].join('\0'),
289    float64Cells: [4.2],
290    isLastBatch: true,
291  });
292  const resProto = QueryResultProto.create({
293    columnNames: ['x', 'y', 'x', 'x', 'y'],
294    batch: [batch],
295  });
296
297  const qr = createQueryResult({query: 'Some query'});
298  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
299  expect(qr.isComplete()).toBe(true);
300  expect(qr.numRows()).toBe(1);
301  expect(qr.columns()).toEqual(['x', 'y', 'x_1', 'x_2', 'y_1']);
302  // First try iterating without selecting any column.
303  {
304    const iter = qr.iter({x: NUM, y: STR, x_1: NUM, x_2: STR, y_1: STR});
305    expect(iter.valid()).toBe(true);
306    expect(iter.x).toBe(42);
307    expect(iter.y).toBe('a');
308    expect(iter.x_1).toBe(4.2);
309    expect(iter.x_2).toBe('b');
310    expect(iter.y_1).toBe('c');
311    iter.next();
312    expect(iter.valid()).toBe(false);
313  }
314  expect(() => qr.iter({x_3: NUM})).toThrowError(/\bx_3\b.*not found/);
315});
316
317test('QueryResult.WaitMoreRows', async () => {
318  const batchA = QueryResultProto.CellsBatch.create({
319    cells: [T.CELL_VARINT],
320    varintCells: [42],
321    isLastBatch: false,
322  });
323  const resProtoA = QueryResultProto.create({
324    columnNames: ['a_int'],
325    batch: [batchA],
326  });
327
328  const qr = createQueryResult({query: 'Some query'});
329  qr.appendResultBatch(QueryResultProto.encode(resProtoA).finish());
330
331  const batchB = QueryResultProto.CellsBatch.create({
332    cells: [T.CELL_VARINT],
333    varintCells: [43],
334    isLastBatch: true,
335  });
336  const resProtoB = QueryResultProto.create({
337    columnNames: [],
338    batch: [batchB],
339  });
340
341  const waitPromise = qr.waitMoreRows();
342  const appendPromise = new Promise<void>((resolve, _) => {
343    setTimeout(() => {
344      qr.appendResultBatch(QueryResultProto.encode(resProtoB).finish());
345      resolve();
346    }, 0);
347  });
348
349  expect(qr.isComplete()).toBe(false);
350  expect(qr.numRows()).toBe(1);
351
352  await Promise.all([waitPromise, appendPromise]);
353
354  expect(qr.isComplete()).toBe(true);
355  expect(qr.numRows()).toBe(2);
356});
357
358describe('decodeInt64Varint', () => {
359  test('Parsing empty input should throw an error', () => {
360    expect(() => decodeInt64Varint(new Uint8Array(), 0)).toThrow(
361      'Index out of range',
362    );
363  });
364
365  test('Parsing single byte positive integers', () => {
366    const testData: Array<[Uint8Array, BigInt]> = [
367      [new Uint8Array([0x00]), 0n],
368      [new Uint8Array([0x01]), 1n],
369      [new Uint8Array([0x7f]), 127n],
370    ];
371
372    testData.forEach(([input, expected]) => {
373      expect(decodeInt64Varint(input, 0)).toEqual(expected);
374    });
375  });
376
377  test('Parsing multi-byte positive integers', () => {
378    const testData: Array<[Uint8Array, BigInt]> = [
379      [new Uint8Array([0x80, 0x01]), 128n],
380      [new Uint8Array([0xff, 0x7f]), 16383n],
381      [new Uint8Array([0x80, 0x80, 0x01]), 16384n],
382      [new Uint8Array([0xff, 0xff, 0x7f]), 2097151n],
383      [
384        new Uint8Array([
385          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
386        ]),
387        9223372036854775807n,
388      ],
389    ];
390
391    testData.forEach(([input, expected]) => {
392      expect(decodeInt64Varint(input, 0)).toEqual(expected);
393    });
394  });
395
396  test('Parsing negative integers', () => {
397    const testData: Array<[Uint8Array, BigInt]> = [
398      [
399        new Uint8Array([
400          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
401        ]),
402        -1n,
403      ],
404      [
405        new Uint8Array([
406          0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
407        ]),
408        -2n,
409      ],
410      [
411        new Uint8Array([
412          0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01,
413        ]),
414        -9223372036854775808n,
415      ],
416    ];
417
418    testData.forEach(([input, expected]) => {
419      expect(decodeInt64Varint(input, 0)).toEqual(expected);
420    });
421  });
422
423  test('Parsing with incomplete varint should throw an error', () => {
424    const testData: Array<Uint8Array> = [
425      new Uint8Array([0x80]),
426      new Uint8Array([0x80, 0x80]),
427    ];
428
429    testData.forEach((input) => {
430      expect(() => decodeInt64Varint(input, 0)).toThrow('Index out of range');
431    });
432  });
433});
434