query.test.ts822 lines · main
| 1 | import { describe, expect, test } from 'vitest' |
| 2 | |
| 3 | import { ident, joinSqlFragments, safeSql } from '../../src/pg-format' |
| 4 | import { Query } from '../../src/query/Query' |
| 5 | import * as QueryUtils from '../../src/query/Query.utils' |
| 6 | import { QueryAction } from '../../src/query/QueryAction' |
| 7 | import { QueryFilter } from '../../src/query/QueryFilter' |
| 8 | import { QueryModifier } from '../../src/query/QueryModifier' |
| 9 | import type { Filter, QueryTable, Sort } from '../../src/query/types' |
| 10 | |
| 11 | describe('Query', () => { |
| 12 | test('from() should create a QueryAction with the correct table', () => { |
| 13 | const query = new Query() |
| 14 | const action = query.from('users', 'public') |
| 15 | |
| 16 | expect(action).toBeInstanceOf(QueryAction) |
| 17 | expect(action['table']).toEqual({ name: 'users', schema: 'public' }) |
| 18 | }) |
| 19 | |
| 20 | test('from() should use "public" as the default schema when not provided', () => { |
| 21 | const query = new Query() |
| 22 | const action = query.from('users') |
| 23 | |
| 24 | expect(action['table']).toEqual({ name: 'users', schema: 'public' }) |
| 25 | }) |
| 26 | }) |
| 27 | |
| 28 | describe('QueryAction', () => { |
| 29 | const table: QueryTable = { name: 'users', schema: 'public' } |
| 30 | |
| 31 | test('count() should create a QueryFilter with the correct action', () => { |
| 32 | const action = new QueryAction(table) |
| 33 | const filter = action.count() |
| 34 | |
| 35 | expect(filter).toBeInstanceOf(QueryFilter) |
| 36 | expect(filter['table']).toEqual(table) |
| 37 | expect(filter['actionConfig'].action).toBe('count') |
| 38 | }) |
| 39 | |
| 40 | test('delete() should create a QueryFilter with the correct action and options', () => { |
| 41 | const action = new QueryAction(table) |
| 42 | const filter = action.delete({ returning: true }) |
| 43 | |
| 44 | expect(filter).toBeInstanceOf(QueryFilter) |
| 45 | expect(filter['table']).toEqual(table) |
| 46 | expect(filter['actionConfig'].action).toBe('delete') |
| 47 | expect(filter['actionOptions']).toEqual({ returning: true }) |
| 48 | }) |
| 49 | |
| 50 | test('insert() should create a QueryFilter with the correct action, values and options', () => { |
| 51 | const action = new QueryAction(table) |
| 52 | const values = [{ id: 1, name: 'John' }] |
| 53 | const filter = action.insert(values, { returning: true }) |
| 54 | |
| 55 | expect(filter).toBeInstanceOf(QueryFilter) |
| 56 | expect(filter['table']).toEqual(table) |
| 57 | expect(filter['actionConfig']).toEqual({ action: 'insert', actionValue: values }) |
| 58 | expect(filter['actionOptions']).toEqual({ returning: true }) |
| 59 | }) |
| 60 | |
| 61 | test('select() should create a QueryFilter with the correct action and columns', () => { |
| 62 | const action = new QueryAction(table) |
| 63 | const cols = joinSqlFragments([ident('id'), ident('name')], ', ') |
| 64 | const filter = action.select(cols) |
| 65 | |
| 66 | expect(filter).toBeInstanceOf(QueryFilter) |
| 67 | expect(filter['table']).toEqual(table) |
| 68 | expect(filter['actionConfig']).toEqual({ action: 'select', actionValue: 'id, name' }) |
| 69 | }) |
| 70 | |
| 71 | test('update() should create a QueryFilter with the correct action, value and options', () => { |
| 72 | const action = new QueryAction(table) |
| 73 | const value = { name: 'John' } |
| 74 | const filter = action.update(value, { returning: true }) |
| 75 | |
| 76 | expect(filter).toBeInstanceOf(QueryFilter) |
| 77 | expect(filter['table']).toEqual(table) |
| 78 | expect(filter['actionConfig']).toEqual({ action: 'update', actionValue: value }) |
| 79 | expect(filter['actionOptions']).toEqual({ returning: true }) |
| 80 | }) |
| 81 | |
| 82 | test('truncate() should create a QueryFilter with the correct action and options', () => { |
| 83 | const action = new QueryAction(table) |
| 84 | const filter = action.truncate({ returning: true }) |
| 85 | |
| 86 | expect(filter).toBeInstanceOf(QueryFilter) |
| 87 | expect(filter['table']).toEqual(table) |
| 88 | expect(filter['actionConfig'].action).toBe('truncate') |
| 89 | expect(filter['actionOptions']).toEqual({ returning: true }) |
| 90 | }) |
| 91 | }) |
| 92 | |
| 93 | describe('QueryFilter', () => { |
| 94 | const table: QueryTable = { name: 'users', schema: 'public' } |
| 95 | |
| 96 | test('filter() should add a filter and return the filter instance', () => { |
| 97 | const queryFilter = new QueryFilter(table, { |
| 98 | action: 'select', |
| 99 | actionValue: joinSqlFragments([ident('id'), ident('name')], ', '), |
| 100 | }) |
| 101 | const result = queryFilter.filter('id', '=', 1) |
| 102 | |
| 103 | expect(result).toBe(queryFilter) |
| 104 | expect(queryFilter['filters']).toEqual([{ column: 'id', operator: '=', value: 1 }]) |
| 105 | }) |
| 106 | |
| 107 | test('match() should add multiple filters and return the filter instance', () => { |
| 108 | const queryFilter = new QueryFilter(table, { |
| 109 | action: 'select', |
| 110 | actionValue: joinSqlFragments([ident('id'), ident('name')], ', '), |
| 111 | }) |
| 112 | const result = queryFilter.match({ id: 1, name: 'John' }) |
| 113 | |
| 114 | expect(result).toBe(queryFilter) |
| 115 | expect(queryFilter['filters']).toEqual([ |
| 116 | { column: 'id', operator: '=', value: 1 }, |
| 117 | { column: 'name', operator: '=', value: 'John' }, |
| 118 | ]) |
| 119 | }) |
| 120 | |
| 121 | test('order() should add a sort and return the filter instance', () => { |
| 122 | const queryFilter = new QueryFilter(table, { |
| 123 | action: 'select', |
| 124 | actionValue: joinSqlFragments([ident('id'), ident('name')], ', '), |
| 125 | }) |
| 126 | const result = queryFilter.order('users', 'name', false, true) |
| 127 | |
| 128 | expect(result).toBe(queryFilter) |
| 129 | expect(queryFilter['sorts']).toEqual([ |
| 130 | { table: 'users', column: 'name', ascending: false, nullsFirst: true }, |
| 131 | ]) |
| 132 | }) |
| 133 | |
| 134 | test('range() should delegate to QueryModifier.range() and return the result', () => { |
| 135 | const queryFilter = new QueryFilter(table, { |
| 136 | action: 'select', |
| 137 | actionValue: joinSqlFragments([ident('id'), ident('name')], ', '), |
| 138 | }) |
| 139 | const result = queryFilter.range(0, 10) |
| 140 | |
| 141 | expect(result).toBeInstanceOf(QueryModifier) |
| 142 | // The pagination gets set in the QueryModifier |
| 143 | expect(result['pagination']).toEqual({ offset: 0, limit: 11 }) |
| 144 | }) |
| 145 | |
| 146 | test('toSql() should delegate to QueryModifier.toSql() and return the SQL string', () => { |
| 147 | const queryFilter = new QueryFilter(table, { |
| 148 | action: 'select', |
| 149 | actionValue: joinSqlFragments([ident('id'), ident('name')], ', '), |
| 150 | }) |
| 151 | queryFilter.filter('id', '=', 1) |
| 152 | |
| 153 | const result = queryFilter.toSql() |
| 154 | |
| 155 | // Expected SQL should match the pattern from QueryUtils.selectQuery() |
| 156 | expect(result).toBe('select id, name from public.users where id = 1;') |
| 157 | }) |
| 158 | }) |
| 159 | |
| 160 | describe('QueryModifier', () => { |
| 161 | const table: QueryTable = { name: 'users', schema: 'public' } |
| 162 | |
| 163 | test('range() should set the pagination and return the modifier instance', () => { |
| 164 | const queryModifier = new QueryModifier(table, { |
| 165 | action: 'select', |
| 166 | actionValue: joinSqlFragments([ident('id'), ident('name')], ', '), |
| 167 | }) |
| 168 | const result = queryModifier.range(0, 10) |
| 169 | |
| 170 | expect(result).toBe(queryModifier) |
| 171 | expect(queryModifier['pagination']).toEqual({ offset: 0, limit: 11 }) |
| 172 | }) |
| 173 | |
| 174 | test('toSql() should generate the correct SQL for a count query', () => { |
| 175 | const queryModifier = new QueryModifier(table, { action: 'count' }) |
| 176 | const result = queryModifier.toSql() |
| 177 | |
| 178 | expect(result).toBe('select count(*) from public.users;') |
| 179 | }) |
| 180 | |
| 181 | test('toSql() should generate the correct SQL for a delete query with filters', () => { |
| 182 | const queryModifier = new QueryModifier( |
| 183 | table, |
| 184 | { action: 'delete' }, |
| 185 | { |
| 186 | filters: [{ column: 'id', operator: '=', value: 1 }], |
| 187 | actionOptions: { returning: true }, |
| 188 | } |
| 189 | ) |
| 190 | const result = queryModifier.toSql() |
| 191 | |
| 192 | expect(result).toBe('delete from public.users where id = 1 returning *;') |
| 193 | }) |
| 194 | |
| 195 | test('toSql() should generate the correct SQL for a select query with filters, sorts and pagination', () => { |
| 196 | const queryModifier = new QueryModifier( |
| 197 | table, |
| 198 | { action: 'select', actionValue: joinSqlFragments([ident('id'), ident('name')], ', ') }, |
| 199 | { |
| 200 | filters: [{ column: 'id', operator: '>', value: 10 }], |
| 201 | sorts: [{ table: 'users', column: 'name', ascending: true, nullsFirst: false }], |
| 202 | } |
| 203 | ) |
| 204 | queryModifier.range(0, 5) |
| 205 | const result = queryModifier.toSql() |
| 206 | expect(result).toMatchInlineSnapshot( |
| 207 | `"select id, name from public.users where id > 10 order by users.name asc nulls last limit 6 offset 0;"` |
| 208 | ) |
| 209 | }) |
| 210 | |
| 211 | test('toSql() should generate the correct SQL for a truncate query', () => { |
| 212 | const queryModifier = new QueryModifier(table, { action: 'truncate' }) |
| 213 | const result = queryModifier.toSql() |
| 214 | |
| 215 | expect(result).toBe('truncate public.users;') |
| 216 | }) |
| 217 | |
| 218 | test('toSql() should generate the correct SQL for a truncate query with cascade', () => { |
| 219 | const queryModifier = new QueryModifier( |
| 220 | table, |
| 221 | { action: 'truncate' }, |
| 222 | { |
| 223 | actionOptions: { cascade: true }, |
| 224 | } |
| 225 | ) |
| 226 | const result = queryModifier.toSql() |
| 227 | |
| 228 | expect(result).toBe('truncate public.users cascade;') |
| 229 | }) |
| 230 | }) |
| 231 | |
| 232 | describe('Query.utils', () => { |
| 233 | const table: QueryTable = { name: 'users', schema: 'public' } |
| 234 | |
| 235 | describe('countQuery', () => { |
| 236 | test('should generate a correct count query without filters', () => { |
| 237 | const result = QueryUtils.countQuery(table) |
| 238 | expect(result).toBe('select count(*) from public.users;') |
| 239 | }) |
| 240 | |
| 241 | test('should generate a correct count query with filters', () => { |
| 242 | const filters = [{ column: 'id', operator: '>' as const, value: 1 }] |
| 243 | const result = QueryUtils.countQuery(table, { filters: filters }) |
| 244 | expect(result).toBe('select count(*) from public.users where id > 1;') |
| 245 | }) |
| 246 | }) |
| 247 | |
| 248 | describe('truncateQuery', () => { |
| 249 | test('should generate a correct truncate query without cascade', () => { |
| 250 | const result = QueryUtils.truncateQuery(table) |
| 251 | expect(result).toBe('truncate public.users;') |
| 252 | }) |
| 253 | |
| 254 | test('should generate a correct truncate query with cascade', () => { |
| 255 | const result = QueryUtils.truncateQuery(table, { cascade: true }) |
| 256 | expect(result).toBe('truncate public.users cascade;') |
| 257 | }) |
| 258 | }) |
| 259 | |
| 260 | describe('deleteQuery', () => { |
| 261 | test('should throw an error if no filters are provided', () => { |
| 262 | expect(() => QueryUtils.deleteQuery(table)).toThrow() |
| 263 | }) |
| 264 | |
| 265 | test('should generate a correct delete query with filters', () => { |
| 266 | const filters = [{ column: 'id', operator: '=' as const, value: 1 }] |
| 267 | const result = QueryUtils.deleteQuery(table, filters) |
| 268 | expect(result).toBe('delete from public.users where id = 1;') |
| 269 | }) |
| 270 | |
| 271 | test('should include returning clause when specified', () => { |
| 272 | const filters = [{ column: 'id', operator: '=' as const, value: 1 }] |
| 273 | const result = QueryUtils.deleteQuery(table, filters, { returning: true }) |
| 274 | expect(result).toBe('delete from public.users where id = 1 returning *;') |
| 275 | }) |
| 276 | |
| 277 | test('should include enum array columns in returning clause when specified', () => { |
| 278 | const filters = [{ column: 'id', operator: '=' as const, value: 1 }] |
| 279 | const result = QueryUtils.deleteQuery(table, filters, { |
| 280 | returning: true, |
| 281 | enumArrayColumns: ['tags'], |
| 282 | }) |
| 283 | expect(result).toBe('delete from public.users where id = 1 returning *, tags::text[];') |
| 284 | }) |
| 285 | }) |
| 286 | |
| 287 | describe('insertQuery', () => { |
| 288 | test('should throw an error if no values are provided', () => { |
| 289 | expect(() => QueryUtils.insertQuery(table, [])).toThrow() |
| 290 | }) |
| 291 | |
| 292 | test('should generate a correct insert query with values', () => { |
| 293 | const values = [{ id: 1, name: 'John' }] |
| 294 | const result = QueryUtils.insertQuery(table, values) |
| 295 | expect(result).toMatchInlineSnapshot( |
| 296 | `"insert into public.users (id,name) select id,name from jsonb_populate_recordset(null::public.users, '[{"id":1,"name":"John"}]');"` |
| 297 | ) |
| 298 | }) |
| 299 | |
| 300 | test('should include returning clause when specified', () => { |
| 301 | const values = [{ id: 1, name: 'John' }] |
| 302 | const result = QueryUtils.insertQuery(table, values, { returning: true }) |
| 303 | expect(result).toMatchInlineSnapshot( |
| 304 | `"insert into public.users (id,name) select id,name from jsonb_populate_recordset(null::public.users, '[{"id":1,"name":"John"}]') returning *;"` |
| 305 | ) |
| 306 | }) |
| 307 | |
| 308 | test('should include enum array columns in returning clause when specified', () => { |
| 309 | const values = [{ id: 1, name: 'John' }] |
| 310 | const result = QueryUtils.insertQuery(table, values, { |
| 311 | returning: true, |
| 312 | enumArrayColumns: ['tags'], |
| 313 | }) |
| 314 | expect(result).toMatchInlineSnapshot( |
| 315 | `"insert into public.users (id,name) select id,name from jsonb_populate_recordset(null::public.users, '[{"id":1,"name":"John"}]') returning *, tags::text[];"` |
| 316 | ) |
| 317 | }) |
| 318 | }) |
| 319 | |
| 320 | describe('selectQuery', () => { |
| 321 | test('should generate a correct select query without options', () => { |
| 322 | const result = QueryUtils.selectQuery(table) |
| 323 | expect(result).toBe('select * from public.users;') |
| 324 | }) |
| 325 | |
| 326 | test('should generate a correct select query with custom columns', () => { |
| 327 | const result = QueryUtils.selectQuery( |
| 328 | table, |
| 329 | joinSqlFragments([ident('id'), ident('name')], ', ') |
| 330 | ) |
| 331 | expect(result).toBe('select id, name from public.users;') |
| 332 | }) |
| 333 | |
| 334 | test('should generate a correct select query with filters', () => { |
| 335 | const filters = [{ column: 'id', operator: '>' as const, value: 1 }] |
| 336 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 337 | expect(result).toBe('select * from public.users where id > 1;') |
| 338 | }) |
| 339 | |
| 340 | test('should generate a correct select query with sorts', () => { |
| 341 | const sorts = [{ table: 'users', column: 'name', ascending: true, nullsFirst: false }] |
| 342 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 343 | expect(result).toBe('select * from public.users order by users.name asc nulls last;') |
| 344 | }) |
| 345 | |
| 346 | test('should generate a correct select query with pagination', () => { |
| 347 | const pagination = { limit: 10, offset: 0 } |
| 348 | const result = QueryUtils.selectQuery(table, safeSql`*`, { pagination: pagination }) |
| 349 | expect(result).toBe('select * from public.users limit 10 offset 0;') |
| 350 | }) |
| 351 | |
| 352 | test('should ignore sorts with undefined column', () => { |
| 353 | const sorts: Sort[] = [{ table: 'users', column: '', ascending: true, nullsFirst: false }] |
| 354 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 355 | expect(result).toMatchInlineSnapshot(`"select * from public.users;"`) |
| 356 | }) |
| 357 | }) |
| 358 | |
| 359 | describe('updateQuery', () => { |
| 360 | test('should throw an error if no filters are provided', () => { |
| 361 | const value = { name: 'John' } |
| 362 | expect(() => QueryUtils.updateQuery(table, value)).toThrow() |
| 363 | }) |
| 364 | |
| 365 | test('should generate a correct update query with filters', () => { |
| 366 | const value = { name: 'John' } |
| 367 | const filters = [{ column: 'id', operator: '=' as const, value: 1 }] |
| 368 | const result = QueryUtils.updateQuery(table, value, { filters: filters }) |
| 369 | expect(result).toMatchInlineSnapshot( |
| 370 | `"update public.users set (name) = (select name from json_populate_record(null::public.users, '{"name":"John"}')) where id = 1;"` |
| 371 | ) |
| 372 | }) |
| 373 | |
| 374 | test('should include returning clause when specified', () => { |
| 375 | const value = { name: 'John' } |
| 376 | const filters = [{ column: 'id', operator: '=' as const, value: 1 }] |
| 377 | const result = QueryUtils.updateQuery(table, value, { |
| 378 | filters: filters, |
| 379 | returning: true, |
| 380 | }) |
| 381 | expect(result).toMatchInlineSnapshot( |
| 382 | `"update public.users set (name) = (select name from json_populate_record(null::public.users, '{"name":"John"}')) where id = 1 returning *;"` |
| 383 | ) |
| 384 | }) |
| 385 | |
| 386 | test('should include enum array columns in returning clause when specified', () => { |
| 387 | const value = { name: 'John' } |
| 388 | const filters = [{ column: 'id', operator: '=' as const, value: 1 }] |
| 389 | const result = QueryUtils.updateQuery(table, value, { |
| 390 | filters: filters, |
| 391 | returning: true, |
| 392 | enumArrayColumns: ['tags'], |
| 393 | }) |
| 394 | expect(result).toMatchInlineSnapshot( |
| 395 | `"update public.users set (name) = (select name from json_populate_record(null::public.users, '{"name":"John"}')) where id = 1 returning *, tags::text[];"` |
| 396 | ) |
| 397 | }) |
| 398 | }) |
| 399 | |
| 400 | describe('Query.utils internal functions', () => { |
| 401 | describe('applyFilters', () => { |
| 402 | test('should correctly apply equality filters', () => { |
| 403 | const filters: Filter[] = [{ column: 'name', operator: '=', value: 'John' }] |
| 404 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 405 | expect(result).toBe("select * from public.users where name = 'John';") |
| 406 | }) |
| 407 | |
| 408 | test('should correctly apply multiple filters with AND logic', () => { |
| 409 | const filters: Filter[] = [ |
| 410 | { column: 'name', operator: '=', value: 'John' }, |
| 411 | { column: 'age', operator: '>', value: 25 }, |
| 412 | ] |
| 413 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 414 | expect(result).toBe("select * from public.users where name = 'John' and age > 25;") |
| 415 | }) |
| 416 | |
| 417 | test('should correctly handle "in" operator with array values', () => { |
| 418 | const filters: Filter[] = [{ column: 'id', operator: 'in', value: [1, 2, 3] }] |
| 419 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 420 | expect(result).toBe('select * from public.users where id in (1,2,3);') |
| 421 | }) |
| 422 | |
| 423 | test('should correctly handle "in" operator with comma-separated string', () => { |
| 424 | const filters: Filter[] = [{ column: 'id', operator: 'in', value: '1,2,3' }] |
| 425 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 426 | expect(result).toBe("select * from public.users where id in ('1','2','3');") |
| 427 | }) |
| 428 | |
| 429 | test('should correctly handle "is" operator with null value', () => { |
| 430 | const filters: Filter[] = [{ column: 'email', operator: 'is', value: 'null' }] |
| 431 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 432 | expect(result).toBe('select * from public.users where email is null;') |
| 433 | }) |
| 434 | |
| 435 | test('should correctly handle "is" operator with not null value', () => { |
| 436 | const filters: Filter[] = [{ column: 'email', operator: 'is', value: 'not null' }] |
| 437 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 438 | expect(result).toBe('select * from public.users where email is not null;') |
| 439 | }) |
| 440 | |
| 441 | test('should correctly handle "is" operator with boolean values', () => { |
| 442 | const filters: Filter[] = [{ column: 'active', operator: 'is', value: 'true' }] |
| 443 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 444 | expect(result).toBe('select * from public.users where active is true;') |
| 445 | }) |
| 446 | |
| 447 | test('should correctly escape string values in filters', () => { |
| 448 | const filters: Filter[] = [{ column: 'name', operator: '=', value: "O'Reilly" }] |
| 449 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 450 | expect(result).toContain("where name = 'O''Reilly'") |
| 451 | }) |
| 452 | |
| 453 | test('should error if tuple filter value length does not match column length', () => { |
| 454 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: [1] }] |
| 455 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError( |
| 456 | 'Tuple filter value must have the same length as the column array' |
| 457 | ) |
| 458 | }) |
| 459 | |
| 460 | test('should error if tuple filter value is not an array', () => { |
| 461 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: 1 }] |
| 462 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError( |
| 463 | 'Tuple filter value must be an array' |
| 464 | ) |
| 465 | }) |
| 466 | |
| 467 | test('should correctly handle tuple filters with equality operator', () => { |
| 468 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: [1, 2] }] |
| 469 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 470 | expect(result).toBe('select * from public.users where (id, version) = (1, 2);') |
| 471 | }) |
| 472 | |
| 473 | test('should correctly handle tuple filters with greater than operator', () => { |
| 474 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '>', value: [1, 2] }] |
| 475 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 476 | expect(result).toBe('select * from public.users where (id, version) > (1, 2);') |
| 477 | }) |
| 478 | |
| 479 | test('should correctly handle tuple filters with greater than or equal operator', () => { |
| 480 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '>=', value: [1, 2] }] |
| 481 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 482 | expect(result).toBe('select * from public.users where (id, version) >= (1, 2);') |
| 483 | }) |
| 484 | |
| 485 | test('should correctly handle tuple filters with less than operator', () => { |
| 486 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '<', value: [10, 5] }] |
| 487 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 488 | expect(result).toBe('select * from public.users where (id, version) < (10, 5);') |
| 489 | }) |
| 490 | |
| 491 | test('should correctly handle tuple filters with less than or equal operator', () => { |
| 492 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '<=', value: [10, 5] }] |
| 493 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 494 | expect(result).toBe('select * from public.users where (id, version) <= (10, 5);') |
| 495 | }) |
| 496 | |
| 497 | test('should correctly handle tuple filters with not equal operator (<>)', () => { |
| 498 | const filters: Filter[] = [{ column: ['id', 'version'], operator: '<>', value: [1, 2] }] |
| 499 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 500 | expect(result).toBe('select * from public.users where (id, version) <> (1, 2);') |
| 501 | }) |
| 502 | |
| 503 | test('should correctly handle tuple filters with in operator', () => { |
| 504 | const filters: Filter[] = [ |
| 505 | { |
| 506 | column: ['id', 'version'], |
| 507 | operator: 'in', |
| 508 | value: [ |
| 509 | [1, 2], |
| 510 | [3, 4], |
| 511 | [5, 6], |
| 512 | ], |
| 513 | }, |
| 514 | ] |
| 515 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 516 | expect(result).toBe( |
| 517 | 'select * from public.users where (id, version) in ((1, 2), (3, 4), (5, 6));' |
| 518 | ) |
| 519 | }) |
| 520 | |
| 521 | test('should error if tuple filters with in operator do not have matching number of array values', () => { |
| 522 | const filters: Filter[] = [ |
| 523 | { |
| 524 | column: ['id', 'version'], |
| 525 | operator: 'in', |
| 526 | value: [[1, 2], [3, 4], [5]], |
| 527 | }, |
| 528 | ] |
| 529 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 530 | }) |
| 531 | |
| 532 | test('should correctly handle tuple filters with in operator using strings', () => { |
| 533 | const filters: Filter[] = [ |
| 534 | { |
| 535 | column: ['id', 'version'], |
| 536 | operator: 'in', |
| 537 | value: ['one,two', 'three,four', 'five,six'], |
| 538 | }, |
| 539 | ] |
| 540 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 541 | expect(result).toBe( |
| 542 | `select * from public.users where (id, version) in (('one', 'two'), ('three', 'four'), ('five', 'six'));` |
| 543 | ) |
| 544 | }) |
| 545 | |
| 546 | test('should error if tuple filters with in operator do not have matching number of stringified values', () => { |
| 547 | const filters: Filter[] = [ |
| 548 | { |
| 549 | column: ['id', 'version'], |
| 550 | operator: 'in', |
| 551 | value: ['one,two', 'three,four', 'five'], |
| 552 | }, |
| 553 | ] |
| 554 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 555 | }) |
| 556 | |
| 557 | test('should correctly handle tuple filters with string values', () => { |
| 558 | const filters: Filter[] = [ |
| 559 | { column: ['first_name', 'last_name'], operator: '=', value: ['John', 'Doe'] }, |
| 560 | ] |
| 561 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 562 | expect(result).toBe( |
| 563 | "select * from public.users where (first_name, last_name) = ('John', 'Doe');" |
| 564 | ) |
| 565 | }) |
| 566 | |
| 567 | test('should correctly handle mixed tuple and regular filters', () => { |
| 568 | const filters: Filter[] = [ |
| 569 | { column: ['id', 'version'], operator: '>', value: [1, 2] }, |
| 570 | { column: 'active', operator: '=', value: true }, |
| 571 | ] |
| 572 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 573 | expect(result).toBe( |
| 574 | 'select * from public.users where (id, version) > (1, 2) and active = true;' |
| 575 | ) |
| 576 | }) |
| 577 | |
| 578 | test('should error when trying to use "is" operator as a tuple filter', () => { |
| 579 | const filters: Filter[] = [ |
| 580 | { |
| 581 | column: ['id', 'version'], |
| 582 | operator: 'is', |
| 583 | value: [null, null], |
| 584 | }, |
| 585 | ] |
| 586 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 587 | }) |
| 588 | |
| 589 | test('should error when trying to use "~~" operator as a tuple filter', () => { |
| 590 | const filters: Filter[] = [ |
| 591 | { |
| 592 | column: ['first_name', 'last_name'], |
| 593 | operator: '~~', |
| 594 | value: ['%John%', '%Doe%'], |
| 595 | }, |
| 596 | ] |
| 597 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 598 | }) |
| 599 | |
| 600 | test('should error when trying to use "~~*" operator as a tuple filter', () => { |
| 601 | const filters: Filter[] = [ |
| 602 | { column: ['first_name', 'last_name'], operator: '~~*', value: ['%john%', '%doe%'] }, |
| 603 | ] |
| 604 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 605 | }) |
| 606 | |
| 607 | test('should error when trying to use "!~~" operator as a tuple filter', () => { |
| 608 | const filters: Filter[] = [ |
| 609 | { column: ['first_name', 'last_name'], operator: '!~~', value: ['%Admin%', '%System%'] }, |
| 610 | ] |
| 611 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 612 | }) |
| 613 | |
| 614 | test('should error when trying to use "!~~*" operator as a tuple filter', () => { |
| 615 | const filters: Filter[] = [ |
| 616 | { column: ['first_name', 'last_name'], operator: '!~~*', value: ['%admin%', '%system%'] }, |
| 617 | ] |
| 618 | expect(() => QueryUtils.selectQuery(table, safeSql`*`, { filters: filters })).toThrowError() |
| 619 | }) |
| 620 | }) |
| 621 | |
| 622 | describe('applySorts', () => { |
| 623 | test('should correctly apply a single sort with default options', () => { |
| 624 | const sorts: Sort[] = [ |
| 625 | { table: 'users', column: 'name', ascending: true, nullsFirst: false }, |
| 626 | ] |
| 627 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 628 | expect(result).toBe('select * from public.users order by users.name asc nulls last;') |
| 629 | }) |
| 630 | |
| 631 | test('should correctly apply a descending sort', () => { |
| 632 | const sorts: Sort[] = [ |
| 633 | { table: 'users', column: 'name', ascending: false, nullsFirst: false }, |
| 634 | ] |
| 635 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 636 | expect(result).toBe('select * from public.users order by users.name desc nulls last;') |
| 637 | }) |
| 638 | |
| 639 | test('should correctly apply nulls first option', () => { |
| 640 | const sorts: Sort[] = [ |
| 641 | { table: 'users', column: 'name', ascending: true, nullsFirst: true }, |
| 642 | ] |
| 643 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 644 | expect(result).toBe('select * from public.users order by users.name asc nulls first;') |
| 645 | }) |
| 646 | |
| 647 | test('should correctly apply multiple sorts', () => { |
| 648 | const sorts: Sort[] = [ |
| 649 | { table: 'users', column: 'last_name', ascending: true, nullsFirst: false }, |
| 650 | { table: 'users', column: 'first_name', ascending: true, nullsFirst: false }, |
| 651 | ] |
| 652 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 653 | expect(result).toBe( |
| 654 | 'select * from public.users order by users.last_name asc nulls last, users.first_name asc nulls last;' |
| 655 | ) |
| 656 | }) |
| 657 | |
| 658 | test('should ignore sorts with undefined column', () => { |
| 659 | const sorts: Sort[] = [{ table: 'users', column: '', ascending: true, nullsFirst: false }] |
| 660 | const result = QueryUtils.selectQuery(table, safeSql`*`, { sorts: sorts }) |
| 661 | expect(result).toMatchInlineSnapshot(`"select * from public.users;"`) |
| 662 | }) |
| 663 | }) |
| 664 | |
| 665 | describe('filterLiteral', () => { |
| 666 | test('should correctly handle array literal syntax', () => { |
| 667 | const filters: Filter[] = [{ column: 'tags', operator: '=', value: "ARRAY['tag1','tag2']" }] |
| 668 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 669 | expect(result).toBe("select * from public.users where tags = ARRAY['tag1','tag2'];") |
| 670 | }) |
| 671 | |
| 672 | test('should correctly handle non-string values', () => { |
| 673 | const filters: Filter[] = [{ column: 'active', operator: '=', value: true }] |
| 674 | const result = QueryUtils.selectQuery(table, safeSql`*`, { filters: filters }) |
| 675 | expect(result).toBe('select * from public.users where active = true;') |
| 676 | }) |
| 677 | }) |
| 678 | |
| 679 | describe('queryTable', () => { |
| 680 | test('should correctly format the table name with schema', () => { |
| 681 | const result = QueryUtils.selectQuery({ name: 'orders', schema: 'shop' }) |
| 682 | expect(result).toBe('select * from shop.orders;') |
| 683 | }) |
| 684 | }) |
| 685 | }) |
| 686 | }) |
| 687 | |
| 688 | describe('End-to-end query chaining', () => { |
| 689 | test('should correctly build a simple select query', () => { |
| 690 | const query = new Query() |
| 691 | const sql = query |
| 692 | .from('users', 'public') |
| 693 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 694 | .toSql() |
| 695 | |
| 696 | expect(sql).toBe('select id, name, email from public.users;') |
| 697 | }) |
| 698 | |
| 699 | test('should correctly build a filtered select query', () => { |
| 700 | const query = new Query() |
| 701 | const sql = query |
| 702 | .from('users', 'public') |
| 703 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 704 | .filter('id', '>', 10) |
| 705 | .toSql() |
| 706 | |
| 707 | expect(sql).toBe('select id, name, email from public.users where id > 10;') |
| 708 | }) |
| 709 | |
| 710 | test('should correctly build a select query with multiple filters', () => { |
| 711 | const query = new Query() |
| 712 | const sql = query |
| 713 | .from('users', 'public') |
| 714 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 715 | .filter('id', '>', 10) |
| 716 | .filter('name', '~~', '%John%') |
| 717 | .toSql() |
| 718 | |
| 719 | expect(sql).toBe( |
| 720 | "select id, name, email from public.users where id > 10 and name::text ~~ '%John%';" |
| 721 | ) |
| 722 | }) |
| 723 | |
| 724 | test('should correctly build a select query with match criteria', () => { |
| 725 | const query = new Query() |
| 726 | const sql = query |
| 727 | .from('users', 'public') |
| 728 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 729 | .match({ active: true, role: 'admin' }) |
| 730 | .toSql() |
| 731 | |
| 732 | expect(sql).toBe( |
| 733 | "select id, name, email from public.users where active = true and role = 'admin';" |
| 734 | ) |
| 735 | }) |
| 736 | |
| 737 | test('should correctly build a select query with sorting', () => { |
| 738 | const query = new Query() |
| 739 | const sql = query |
| 740 | .from('users', 'public') |
| 741 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 742 | .order('users', 'name', true, false) |
| 743 | .toSql() |
| 744 | |
| 745 | expect(sql).toBe('select id, name, email from public.users order by users.name asc nulls last;') |
| 746 | }) |
| 747 | |
| 748 | test('should correctly build a select query with pagination', () => { |
| 749 | const query = new Query() |
| 750 | const sql = query |
| 751 | .from('users', 'public') |
| 752 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 753 | .range(0, 9) |
| 754 | .toSql() |
| 755 | |
| 756 | expect(sql).toBe('select id, name, email from public.users limit 10 offset 0;') |
| 757 | }) |
| 758 | |
| 759 | test('should correctly build a complete select query with filters, sorting and pagination', () => { |
| 760 | const query = new Query() |
| 761 | const sql = query |
| 762 | .from('users', 'public') |
| 763 | .select(joinSqlFragments([ident('id'), ident('name'), ident('email')], ', ')) |
| 764 | .filter('id', '>', 10) |
| 765 | .match({ active: true }) |
| 766 | .order('users', 'name', true, false) |
| 767 | .range(0, 9) |
| 768 | .toSql() |
| 769 | |
| 770 | expect(sql).toBe( |
| 771 | 'select id, name, email from public.users where id > 10 and active = true order by users.name asc nulls last limit 10 offset 0;' |
| 772 | ) |
| 773 | }) |
| 774 | |
| 775 | test('should correctly build an insert query', () => { |
| 776 | const query = new Query() |
| 777 | const sql = query |
| 778 | .from('users', 'public') |
| 779 | .insert([{ name: 'John', email: 'john@example.com' }], { returning: true }) |
| 780 | .toSql() |
| 781 | expect(sql).toMatchInlineSnapshot( |
| 782 | `"insert into public.users (name,email) select name,email from jsonb_populate_recordset(null::public.users, '[{"name":"John","email":"john@example.com"}]') returning *;"` |
| 783 | ) |
| 784 | }) |
| 785 | |
| 786 | test('should correctly build an update query', () => { |
| 787 | const query = new Query() |
| 788 | const sql = query |
| 789 | .from('users', 'public') |
| 790 | .update({ name: 'Updated Name' }, { returning: true }) |
| 791 | .filter('id', '=', 1) |
| 792 | .toSql() |
| 793 | expect(sql).toMatchInlineSnapshot( |
| 794 | `"update public.users set (name) = (select name from json_populate_record(null::public.users, '{"name":"Updated Name"}')) where id = 1 returning *;"` |
| 795 | ) |
| 796 | }) |
| 797 | |
| 798 | test('should correctly build a delete query', () => { |
| 799 | const query = new Query() |
| 800 | const sql = query |
| 801 | .from('users', 'public') |
| 802 | .delete({ returning: true }) |
| 803 | .filter('id', '=', 1) |
| 804 | .toSql() |
| 805 | |
| 806 | expect(sql).toBe('delete from public.users where id = 1 returning *;') |
| 807 | }) |
| 808 | |
| 809 | test('should correctly build a count query', () => { |
| 810 | const query = new Query() |
| 811 | const sql = query.from('users', 'public').count().filter('active', '=', true).toSql() |
| 812 | |
| 813 | expect(sql).toBe('select count(*) from public.users where active = true;') |
| 814 | }) |
| 815 | |
| 816 | test('should correctly build a truncate query', () => { |
| 817 | const query = new Query() |
| 818 | const sql = query.from('users', 'public').truncate().toSql() |
| 819 | |
| 820 | expect(sql).toBe('truncate public.users;') |
| 821 | }) |
| 822 | }) |