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