1// Copyright (C) 2024 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 {SourceDataset, UnionDataset} from './dataset'; 16import { 17 BLOB, 18 BLOB_NULL, 19 LONG, 20 LONG_NULL, 21 NUM, 22 NUM_NULL, 23 STR, 24 STR_NULL, 25 UNKNOWN, 26} from './query_result'; 27 28test('get query for simple dataset', () => { 29 const dataset = new SourceDataset({ 30 src: 'slice', 31 schema: {id: NUM}, 32 }); 33 34 expect(dataset.query()).toEqual('select id from (slice)'); 35}); 36 37test("get query for simple dataset with 'eq' filter", () => { 38 const dataset = new SourceDataset({ 39 src: 'slice', 40 schema: {id: NUM}, 41 filter: { 42 col: 'id', 43 eq: 123, 44 }, 45 }); 46 47 expect(dataset.query()).toEqual('select id from (slice) where id = 123'); 48}); 49 50test("get query for simple dataset with an 'in' filter", () => { 51 const dataset = new SourceDataset({ 52 src: 'slice', 53 schema: {id: NUM}, 54 filter: { 55 col: 'id', 56 in: [123, 456], 57 }, 58 }); 59 60 expect(dataset.query()).toEqual( 61 'select id from (slice) where id in (123,456)', 62 ); 63}); 64 65test('get query for union dataset', () => { 66 const dataset = new UnionDataset([ 67 new SourceDataset({ 68 src: 'slice', 69 schema: {id: NUM}, 70 filter: { 71 col: 'id', 72 eq: 123, 73 }, 74 }), 75 new SourceDataset({ 76 src: 'slice', 77 schema: {id: NUM}, 78 filter: { 79 col: 'id', 80 eq: 456, 81 }, 82 }), 83 ]); 84 85 expect(dataset.query()).toEqual( 86 'select id from (slice) where id = 123\nunion all\nselect id from (slice) where id = 456', 87 ); 88}); 89 90test('union dataset batches large numbers of unions', () => { 91 const datasets = []; 92 for (let i = 0; i < 800; i++) { 93 datasets.push( 94 new SourceDataset({ 95 src: 'foo', 96 schema: {bar: NUM}, 97 filter: { 98 col: 'some_id', 99 eq: i, 100 }, 101 }), 102 ); 103 } 104 105 const query = new UnionDataset(datasets).query(); 106 107 // Verify query structure with CTE batching. 108 expect(query).toContain('with'); 109 110 // Should have at least 2 CTE batches. 111 expect(query).toContain('union_batch_0 as'); 112 expect(query).toContain('union_batch_1 as'); 113 114 // 798 union alls within batches (for 800 datasets) + 1 union alls between the 115 // 2 CTEs. 116 const batchMatches = query.match(/union all/g); 117 expect(batchMatches?.length).toBe(799); 118}); 119 120test('implements', () => { 121 const dataset = new SourceDataset({ 122 src: 'slice', 123 schema: {id: NUM, ts: LONG}, 124 }); 125 126 expect(dataset.implements({id: NUM})).toBe(true); 127 expect(dataset.implements({id: NUM, ts: LONG})).toBe(true); 128 expect(dataset.implements({id: NUM, ts: LONG, name: STR})).toBe(false); 129 expect(dataset.implements({id: LONG})).toBe(false); 130}); 131 132test('implements with relaxed compat checks on optional types', () => { 133 expect( 134 new SourceDataset({ 135 src: 'slice', 136 schema: {foo: NUM_NULL, bar: LONG_NULL, baz: STR_NULL, qux: BLOB_NULL}, 137 }).implements({ 138 foo: NUM_NULL, 139 bar: LONG_NULL, 140 baz: STR_NULL, 141 qux: BLOB_NULL, 142 }), 143 ).toBe(true); 144 145 expect( 146 new SourceDataset({ 147 src: 'slice', 148 schema: {foo: NUM, bar: LONG, baz: STR, qux: BLOB}, 149 }).implements({ 150 foo: NUM_NULL, 151 bar: LONG_NULL, 152 baz: STR_NULL, 153 qux: BLOB_NULL, 154 }), 155 ).toBe(true); 156 157 expect( 158 new SourceDataset({ 159 src: 'slice', 160 schema: {foo: NUM_NULL, bar: LONG_NULL, baz: STR_NULL, qux: BLOB_NULL}, 161 }).implements({ 162 foo: NUM, 163 bar: LONG, 164 baz: STR, 165 qux: BLOB, 166 }), 167 ).toBe(false); 168}); 169 170test('find the schema of a simple dataset', () => { 171 const dataset = new SourceDataset({ 172 src: 'slice', 173 schema: {id: NUM, ts: LONG}, 174 }); 175 176 expect(dataset.schema).toMatchObject({id: NUM, ts: LONG}); 177}); 178 179test('find the schema of a union where source sets differ in their names', () => { 180 const dataset = new UnionDataset([ 181 new SourceDataset({ 182 src: 'slice', 183 schema: {foo: NUM}, 184 }), 185 new SourceDataset({ 186 src: 'slice', 187 schema: {bar: NUM}, 188 }), 189 ]); 190 191 expect(dataset.schema).toMatchObject({}); 192}); 193 194test('find the schema of a union with differing source sets', () => { 195 const dataset = new UnionDataset([ 196 new SourceDataset({ 197 src: 'slice', 198 schema: {foo: NUM}, 199 }), 200 new SourceDataset({ 201 src: 'slice', 202 schema: {foo: LONG}, 203 }), 204 ]); 205 206 expect(dataset.schema).toMatchObject({}); 207}); 208 209test('find the schema of a union with one column in common', () => { 210 const dataset = new UnionDataset([ 211 new SourceDataset({ 212 src: 'slice', 213 schema: {foo: NUM, bar: NUM}, 214 }), 215 new SourceDataset({ 216 src: 'slice', 217 schema: {foo: NUM, baz: NUM}, 218 }), 219 ]); 220 221 expect(dataset.schema).toMatchObject({foo: NUM}); 222}); 223 224test('optimize a union dataset', () => { 225 const dataset = new UnionDataset([ 226 new SourceDataset({ 227 src: 'slice', 228 schema: {}, 229 filter: { 230 col: 'track_id', 231 eq: 123, 232 }, 233 }), 234 new SourceDataset({ 235 src: 'slice', 236 schema: {}, 237 filter: { 238 col: 'track_id', 239 eq: 456, 240 }, 241 }), 242 ]); 243 244 expect(dataset.optimize()).toEqual({ 245 src: 'slice', 246 schema: {}, 247 filter: { 248 col: 'track_id', 249 in: [123, 456], 250 }, 251 }); 252}); 253 254test('optimize a union dataset with different types of filters', () => { 255 const dataset = new UnionDataset([ 256 new SourceDataset({ 257 src: 'slice', 258 schema: {}, 259 filter: { 260 col: 'track_id', 261 eq: 123, 262 }, 263 }), 264 new SourceDataset({ 265 src: 'slice', 266 schema: {}, 267 filter: { 268 col: 'track_id', 269 in: [456, 789], 270 }, 271 }), 272 ]); 273 274 expect(dataset.optimize()).toEqual({ 275 src: 'slice', 276 schema: {}, 277 filter: { 278 col: 'track_id', 279 in: [123, 456, 789], 280 }, 281 }); 282}); 283 284test('optimize a union dataset with different schemas', () => { 285 const dataset = new UnionDataset([ 286 new SourceDataset({ 287 src: 'slice', 288 schema: {foo: NUM}, 289 }), 290 new SourceDataset({ 291 src: 'slice', 292 schema: {bar: NUM}, 293 }), 294 ]); 295 296 expect(dataset.optimize()).toEqual({ 297 src: 'slice', 298 // The resultant schema is the combination of the union's member's schemas, 299 // as we know the source is the same as we know we can get all of the 'seen' 300 // columns from the source. 301 schema: { 302 foo: NUM, 303 bar: NUM, 304 }, 305 }); 306}); 307 308test('union type widening', () => { 309 const dataset = new UnionDataset([ 310 new SourceDataset({ 311 src: 'slice', 312 schema: {foo: NUM, bar: STR_NULL, baz: BLOB, missing: UNKNOWN}, 313 }), 314 new SourceDataset({ 315 src: 'slice', 316 schema: {foo: NUM_NULL, bar: STR, baz: LONG}, 317 }), 318 ]); 319 320 expect(dataset.schema).toEqual({ 321 foo: NUM_NULL, 322 bar: STR_NULL, 323 baz: UNKNOWN, 324 }); 325}); 326