feat: 🎸 Add support for joins with filters (#3010)
This commit adds support for being able to join to other tables when filtering. Support was also added for being able to find null values.
This commit is contained in:
1 parent
fa18bfff01
commit
fda1f56ad1
3 files changed
+348
-132
No files matched your search
@@ -30,7 +30,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "token"
|
||||
WHERE (id = ?)`.removeExtraWhiteSpace(),
|
||||
WHERE ("token".id = ?)`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, ['tokenKey']);
|
||||
});
|
||||
@@ -45,7 +45,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
sql,
|
||||
`
|
||||
SELECT count(*) as total FROM "target"
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, []);
|
||||
});
|
||||
@@ -55,26 +55,26 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
{
|
||||
'single field': {
|
||||
select: [{ field: 'name' }],
|
||||
expectedSelect: 'name',
|
||||
expectedSelect: '"target".name',
|
||||
},
|
||||
'multiple fields': {
|
||||
select: [{ field: 'name' }, { field: 'type' }, { field: 'id' }],
|
||||
expectedSelect: 'name, type, id',
|
||||
expectedSelect: '"target".name, "target".type, "target".id',
|
||||
},
|
||||
'field with alias': {
|
||||
select: [{ field: 'name', alias: 'resource_name' }],
|
||||
expectedSelect: 'name as resource_name',
|
||||
expectedSelect: '"target".name as resource_name',
|
||||
},
|
||||
'multiple fields with aliases': {
|
||||
select: [
|
||||
{ field: 'name', alias: 'resource_name' },
|
||||
{ field: 'type', alias: 'resource_type' },
|
||||
],
|
||||
expectedSelect: 'name as resource_name, type as resource_type',
|
||||
expectedSelect: '"target".name as resource_name, "target".type as resource_type',
|
||||
},
|
||||
'count with alias': {
|
||||
select: [{ field: 'id', isCount: true, alias: 'total_count' }],
|
||||
expectedSelect: 'count(id) as total_count',
|
||||
expectedSelect: 'count("target".id) as total_count',
|
||||
},
|
||||
'count without alias': {
|
||||
select: [{ field: '*', isCount: true }],
|
||||
@@ -82,7 +82,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
},
|
||||
'count distinct': {
|
||||
select: [{ field: 'status', isCount: true, isDistinct: true }],
|
||||
expectedSelect: 'count(DISTINCT status)',
|
||||
expectedSelect: 'count(DISTINCT "target".status)',
|
||||
},
|
||||
'count distinct with alias': {
|
||||
select: [
|
||||
@@ -93,43 +93,43 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
alias: 'unique_types',
|
||||
},
|
||||
],
|
||||
expectedSelect: 'count(DISTINCT type) as unique_types',
|
||||
expectedSelect: 'count(DISTINCT "target".type) as unique_types',
|
||||
},
|
||||
'mixed regular and count fields': {
|
||||
select: [
|
||||
{ field: 'name' },
|
||||
{ field: 'id', isCount: true, alias: 'count' },
|
||||
],
|
||||
expectedSelect: 'name, count(id) as count',
|
||||
expectedSelect: '"target".name, count("target".id) as count',
|
||||
},
|
||||
'field with distinct': {
|
||||
select: [{ field: 'type', isDistinct: true }],
|
||||
expectedSelect: 'DISTINCT type',
|
||||
expectedSelect: 'DISTINCT "target".type',
|
||||
},
|
||||
'multiple distinct fields': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true },
|
||||
{ field: 'status', isDistinct: true },
|
||||
],
|
||||
expectedSelect: 'DISTINCT type, status',
|
||||
expectedSelect: 'DISTINCT "target".type, "target".status',
|
||||
},
|
||||
'multiple distinct with count fields': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true, isCount: true },
|
||||
{ field: 'status', isDistinct: true, isCount: true },
|
||||
],
|
||||
expectedSelect: 'count(DISTINCT type), count(DISTINCT status)',
|
||||
expectedSelect: 'count(DISTINCT "target".type), count(DISTINCT "target".status)',
|
||||
},
|
||||
'distinct with alias': {
|
||||
select: [{ field: 'type', isDistinct: true, alias: 'unique_type' }],
|
||||
expectedSelect: 'DISTINCT type as unique_type',
|
||||
expectedSelect: 'DISTINCT "target".type as unique_type',
|
||||
},
|
||||
'multiple distinct fields with aliases': {
|
||||
select: [
|
||||
{ field: 'type', isDistinct: true, alias: 'unique_type' },
|
||||
{ field: 'status', isDistinct: true, alias: 'unique_status' },
|
||||
],
|
||||
expectedSelect: 'DISTINCT type as unique_type, status as unique_status',
|
||||
expectedSelect: 'DISTINCT "target".type as unique_type, "target".status as unique_status',
|
||||
},
|
||||
'count distinct multiple fields': {
|
||||
select: [
|
||||
@@ -147,7 +147,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
},
|
||||
],
|
||||
expectedSelect:
|
||||
'count(DISTINCT type) as type_count, count(DISTINCT status) as status_count',
|
||||
'count(DISTINCT "target".type) as type_count, count(DISTINCT "target".status) as status_count',
|
||||
},
|
||||
},
|
||||
function (assert, { select, expectedSelect }) {
|
||||
@@ -161,7 +161,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
sql,
|
||||
`
|
||||
SELECT ${expectedSelect} FROM "target"
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, []);
|
||||
},
|
||||
@@ -176,7 +176,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
type: [{ equals: 'ssh' }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (type = ?)',
|
||||
expectedWhereClause: 'WHERE ("target".type = ?)',
|
||||
expectedParams: ['ssh'],
|
||||
},
|
||||
notEquals: {
|
||||
@@ -185,7 +185,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
type: [{ notEquals: 'ssh' }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (type != ?)',
|
||||
expectedWhereClause: 'WHERE ("target".type != ?)',
|
||||
expectedParams: ['ssh'],
|
||||
},
|
||||
contains: {
|
||||
@@ -194,7 +194,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
type: [{ contains: 'ssh' }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (type LIKE ?)',
|
||||
expectedWhereClause: 'WHERE ("target".type LIKE ?)',
|
||||
expectedParams: ['%ssh%'],
|
||||
},
|
||||
greaterThan: {
|
||||
@@ -204,7 +204,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
numberField: [{ gt: 10 }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (created_time > ?) AND (numberField > ?)',
|
||||
expectedWhereClause:
|
||||
'WHERE ("target".created_time > ?) AND ("target".numberField > ?)',
|
||||
expectedParams: [isoDateString, 10],
|
||||
},
|
||||
lessThan: {
|
||||
@@ -214,7 +215,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
numberField: [{ lt: 10 }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (created_time < ?) AND (numberField < ?)',
|
||||
expectedWhereClause:
|
||||
'WHERE ("target".created_time < ?) AND ("target".numberField < ?)',
|
||||
expectedParams: [isoDateString, 10],
|
||||
},
|
||||
greaterThanOrEqual: {
|
||||
@@ -224,7 +226,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
numberField: [{ gte: 10 }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (created_time >= ?) AND (numberField >= ?)',
|
||||
expectedWhereClause:
|
||||
'WHERE ("target".created_time >= ?) AND ("target".numberField >= ?)',
|
||||
expectedParams: [isoDateString, 10],
|
||||
},
|
||||
lessThanOrEqual: {
|
||||
@@ -234,7 +237,8 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
numberField: [{ lte: 10 }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE (created_time <= ?) AND (numberField <= ?)',
|
||||
expectedWhereClause:
|
||||
'WHERE ("target".created_time <= ?) AND ("target".numberField <= ?)',
|
||||
expectedParams: [isoDateString, 10],
|
||||
},
|
||||
logicalOperators: {
|
||||
@@ -252,9 +256,42 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
},
|
||||
},
|
||||
expectedWhereClause:
|
||||
'WHERE (id NOT IN (?, ?)) AND (status IN (?, ?)) AND (type = ?)',
|
||||
'WHERE ("target".id NOT IN (?, ?)) AND ("target".status IN (?, ?)) AND ("target".type = ?)',
|
||||
expectedParams: ['id1', 'id2', 'active', 'pending', 'ssh'],
|
||||
},
|
||||
equalsNull: {
|
||||
query: {
|
||||
filters: {
|
||||
description: [{ equals: null }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE ("target".description IS NULL)',
|
||||
expectedParams: [],
|
||||
},
|
||||
notEqualsNull: {
|
||||
query: {
|
||||
filters: {
|
||||
description: [{ notEquals: null }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause: 'WHERE ("target".description IS NOT NULL)',
|
||||
expectedParams: [],
|
||||
},
|
||||
mixedNullAndValues: {
|
||||
query: {
|
||||
filters: {
|
||||
status: [
|
||||
{ equals: 'active' },
|
||||
{ equals: 'pending' },
|
||||
{ equals: null },
|
||||
],
|
||||
type: [{ notEquals: null }, { notEquals: 'ssh' }],
|
||||
},
|
||||
},
|
||||
expectedWhereClause:
|
||||
'WHERE ("target".status IN (?, ?) OR "target".status IS NULL) AND ("target".type NOT IN (?) OR "target".type IS NOT NULL)',
|
||||
expectedParams: ['active', 'pending', 'ssh'],
|
||||
},
|
||||
},
|
||||
|
||||
function (assert, { query, expectedWhereClause, expectedParams }) {
|
||||
@@ -264,7 +301,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
${expectedWhereClause}
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, expectedParams);
|
||||
},
|
||||
@@ -286,7 +323,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
ORDER BY name COLLATE NOCASE ${expectedDirection}, name ${expectedDirection}`.removeExtraWhiteSpace(),
|
||||
ORDER BY "target".name COLLATE NOCASE ${expectedDirection}, "target".name ${expectedDirection}`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, []);
|
||||
},
|
||||
@@ -300,20 +337,20 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
attributes: ['name', 'id'],
|
||||
isCoalesced: true,
|
||||
},
|
||||
expectedOrderByClause: `ORDER BY COALESCE(name, id) COLLATE NOCASE DESC, COALESCE(name, id) DESC`,
|
||||
expectedOrderByClause: `ORDER BY COALESCE("target".name, "target".id) COLLATE NOCASE DESC, COALESCE("target".name, "target".id) DESC`,
|
||||
},
|
||||
'sort on multiple attributes': {
|
||||
sort: {
|
||||
attributes: ['name', 'id'],
|
||||
},
|
||||
expectedOrderByClause: `ORDER BY name COLLATE NOCASE DESC, id COLLATE NOCASE DESC, name DESC, id DESC`,
|
||||
expectedOrderByClause: `ORDER BY "target".name COLLATE NOCASE DESC, "target".id COLLATE NOCASE DESC, "target".name DESC, "target".id DESC`,
|
||||
},
|
||||
'sort on mapped attributes': {
|
||||
sort: {
|
||||
attributes: ['type'],
|
||||
customSort: { attributeMap: { ssh: 'SSH', tcp: 'Generic TCP' } },
|
||||
},
|
||||
expectedOrderByClause: `ORDER BY CASE type WHEN 'ssh' THEN 'SSH' WHEN 'tcp' THEN 'Generic TCP' END DESC`,
|
||||
expectedOrderByClause: `ORDER BY CASE "target".type WHEN 'ssh' THEN 'SSH' WHEN 'tcp' THEN 'Generic TCP' END DESC`,
|
||||
},
|
||||
},
|
||||
function (assert, { sort, expectedOrderByClause }) {
|
||||
@@ -349,66 +386,131 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
ORDER BY created_time DESC
|
||||
ORDER BY "target".created_time DESC
|
||||
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, expectedParams);
|
||||
},
|
||||
);
|
||||
|
||||
test('it generates FTS5 search with filters', function (assert) {
|
||||
const query = {
|
||||
search: 'favorite',
|
||||
filters: {
|
||||
id: {
|
||||
logicalOperator: 'and',
|
||||
values: [{ notEquals: 'id1' }, { notEquals: 'id2' }],
|
||||
test.each(
|
||||
'it generates FTS5 search correctly',
|
||||
{
|
||||
'string search with filters': {
|
||||
query: {
|
||||
search: 'favorite',
|
||||
},
|
||||
status: [{ equals: 'active' }, { equals: 'pending' }],
|
||||
expectedSql: `
|
||||
SELECT * FROM "target"
|
||||
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY "target".created_time DESC`,
|
||||
expectedParams: ['"favorite"*'],
|
||||
},
|
||||
};
|
||||
|
||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
||||
assert.strictEqual(
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
WHERE (id NOT IN (?, ?)) AND (status IN (?, ?)) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, [
|
||||
'id1',
|
||||
'id2',
|
||||
'active',
|
||||
'pending',
|
||||
'"favorite"*',
|
||||
]);
|
||||
});
|
||||
|
||||
test('it generates FTS5 search with object parameter', function (assert) {
|
||||
const query = {
|
||||
search: {
|
||||
text: 'favorite',
|
||||
fields: ['name', 'description'],
|
||||
'object search with field-specific searches': {
|
||||
query: {
|
||||
search: {
|
||||
text: 'favorite',
|
||||
fields: ['name', 'description'],
|
||||
},
|
||||
},
|
||||
expectedSql: `
|
||||
SELECT * FROM "target"
|
||||
WHERE "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY "target".created_time DESC`,
|
||||
expectedParams: ['name:"favorite"* OR description:"favorite"*'],
|
||||
},
|
||||
filters: {
|
||||
type: [{ equals: 'ssh' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
function (assert, { query, expectedSql, expectedParams }) {
|
||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
||||
assert.strictEqual(sql, expectedSql.removeExtraWhiteSpace());
|
||||
assert.deepEqual(parameters, expectedParams);
|
||||
},
|
||||
);
|
||||
|
||||
const { sql, parameters } = generateSQLExpressions('target', query);
|
||||
assert.strictEqual(
|
||||
sql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
WHERE (type = ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(parameters, [
|
||||
'ssh',
|
||||
'name:"favorite"* OR description:"favorite"*',
|
||||
]);
|
||||
});
|
||||
test.each(
|
||||
'it generates joins with filters',
|
||||
{
|
||||
'basic joins with filters': {
|
||||
query: {
|
||||
filters: {
|
||||
type: [{ equals: 'ssh' }],
|
||||
joins: [
|
||||
{
|
||||
resource: 'session',
|
||||
query: {
|
||||
filters: {
|
||||
status: [{ equals: 'active' }],
|
||||
},
|
||||
},
|
||||
joinOn: 'target_id',
|
||||
joinType: 'INNER',
|
||||
},
|
||||
{
|
||||
resource: 'session-recording',
|
||||
query: {
|
||||
filters: {
|
||||
state: [{ equals: 'available' }],
|
||||
},
|
||||
},
|
||||
joinFrom: 'idx',
|
||||
joinOn: 'target_id',
|
||||
joinType: 'LEFT',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
select: [{ field: 'data' }],
|
||||
expectedSql: `
|
||||
SELECT "target".data FROM "target"
|
||||
INNER JOIN "session" session1 ON "target".id = session1.target_id LEFT JOIN "session_recording" session_recording1 ON "target".idx = session_recording1.target_id
|
||||
WHERE ("target".type = ?) AND ("session1".status = ?) AND ("session_recording1".state = ?)
|
||||
ORDER BY "target".created_time DESC`,
|
||||
expectedParams: ['ssh', 'active', 'available'],
|
||||
},
|
||||
'multiple joins same table': {
|
||||
query: {
|
||||
filters: {
|
||||
joins: [
|
||||
{
|
||||
resource: 'session',
|
||||
query: {
|
||||
filters: {
|
||||
status: [{ equals: 'active' }],
|
||||
},
|
||||
},
|
||||
joinOn: 'target_id',
|
||||
joinType: 'INNER',
|
||||
},
|
||||
{
|
||||
resource: 'session',
|
||||
query: {
|
||||
filters: {
|
||||
status: [{ equals: 'pending' }],
|
||||
},
|
||||
},
|
||||
joinOn: 'host_id',
|
||||
joinType: 'LEFT',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
select: [{ field: 'id' }],
|
||||
expectedSql: `
|
||||
SELECT "target".id FROM "target"
|
||||
INNER JOIN "session" session1 ON "target".id = session1.target_id LEFT JOIN "session" session2 ON "target".id = session2.host_id
|
||||
WHERE ("session1".status = ?) AND ("session2".status = ?)
|
||||
ORDER BY "target".created_time DESC`,
|
||||
expectedParams: ['active', 'pending'],
|
||||
},
|
||||
},
|
||||
function (assert, { query, select, expectedSql, expectedParams }) {
|
||||
const { sql, parameters } = generateSQLExpressions('target', query, {
|
||||
select,
|
||||
});
|
||||
assert.strictEqual(sql, expectedSql.removeExtraWhiteSpace());
|
||||
assert.deepEqual(parameters, expectedParams);
|
||||
},
|
||||
);
|
||||
|
||||
test('it generates SQL with all clauses combined', function (assert) {
|
||||
const query = {
|
||||
@@ -433,9 +535,9 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
assert.strictEqual(
|
||||
sql,
|
||||
`
|
||||
SELECT data FROM "target"
|
||||
WHERE (type = ?) AND (status IN (?, ?)) AND (created_time >= ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY name COLLATE NOCASE DESC, name DESC
|
||||
SELECT "target".data FROM "target"
|
||||
WHERE ("target".type = ?) AND ("target".status IN (?, ?)) AND ("target".created_time >= ?) AND "target".rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?)
|
||||
ORDER BY "target".name COLLATE NOCASE DESC, "target".name DESC
|
||||
LIMIT ? OFFSET ?`.removeExtraWhiteSpace(),
|
||||
);
|
||||
|
||||
@@ -506,7 +608,7 @@ module('Unit | Utility | sqlite-query', function (hooks) {
|
||||
targetSql,
|
||||
`
|
||||
SELECT * FROM "target"
|
||||
ORDER BY created_time DESC`.removeExtraWhiteSpace(),
|
||||
ORDER BY "target".created_time DESC`.removeExtraWhiteSpace(),
|
||||
);
|
||||
assert.deepEqual(targetParameters, []);
|
||||
|
||||
|
||||
Reference in new issue
Block a user