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