Skip to content

Commit

Permalink
Basic filters for PostgreSQL driver (directus#19005)
Browse files Browse the repository at this point in the history
* conversion to abstract sql

* greater than for where clause in pg driver

* refactoring

* forward targets again

* more filter options and fixes

* added filters, moved operator mapping into driver

* removed trim

* add changeset

* removed first type approach

* enhanced abstract sql

* value and set comparison in abstract query

* split up where and intersect again

* preps for where comparison to a set

* typedocs

* fix linter

* The random things we did, who knows

* adding logical conditions (WIP)

Co-authored-by: Rijk van Zanten <[email protected]>
Co-authored-by: Nicola Krumschmidt <[email protected]>

* Push values to parameters directly

* Fix convert test

* Fix convert filter tests

* Implement filter conversion for logical and negate nodes

* Fix where tests

* Add logical where stringification

* Negate comparison operators directly

* Remove some unneeded parentheses

* Improve parameter index generator type

* Fix formatter issues

* Add todo statements

* Remove unused node type

* Removed unneeded check

* One more unnecessary check

---------

Co-authored-by: rijkvanzanten <[email protected]>
Co-authored-by: Nicola Krumschmidt <[email protected]>
Co-authored-by: Nicola Krumschmidt <[email protected]>
  • Loading branch information
4 people committed Jul 2, 2023
1 parent 66f9169 commit 84bd5d0
Show file tree
Hide file tree
Showing 16 changed files with 970 additions and 48 deletions.
7 changes: 7 additions & 0 deletions .changeset/beige-peas-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@directus/data-driver-postgres': minor
'@directus/data-sql': minor
'@directus/data': minor
---

Added basic filters for PostgreSQL driver
26 changes: 23 additions & 3 deletions packages/data-driver-postgres/src/query/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AbstractQueryFieldNodePrimitive } from '@directus/data';
import type { AbstractSqlQuery } from '@directus/data-sql';
import type { AbstractSqlQuery, CompareValueNode } from '@directus/data-sql';
import { randomIdentifier, randomInteger } from '@directus/random';
import { beforeEach, expect, test } from 'vitest';
import { constructSqlQuery } from './index.js';
Expand Down Expand Up @@ -79,7 +79,23 @@ test('statement with order', () => {
test('statement with all possible modifiers', () => {
sample.statement.limit = { parameterIndex: 0 };
sample.statement.offset = { parameterIndex: 1 };
sample.statement.parameters = [randomInteger(1, 100), randomInteger(1, 100)];

sample.statement.where = {
type: 'condition',
operation: 'gt',
target: {
type: 'primitive',
column: randomIdentifier(),
table: randomIdentifier(),
},
compareTo: {
type: 'value',
parameterIndexes: [2],
},
negation: false,
};

sample.statement.parameters = [randomInteger(1, 100), randomInteger(1, 100), randomInteger(1, 100)];

sample.statement.order = [
{
Expand All @@ -94,7 +110,11 @@ test('statement with all possible modifiers', () => {
expect(constructSqlQuery(sample.statement)).toEqual({
statement: `SELECT "${sample.statement.select[0]!.table}"."${sample.statement.select[0]!.column}", "${
sample.statement.select[1]!.table
}"."${sample.statement.select[1]!.column}" FROM "${sample.statement.from}" ORDER BY "${
}"."${sample.statement.select[1]!.column}" FROM "${sample.statement.from}" WHERE "${
sample.statement.where.target.table
}"."${sample.statement.where.target.column}" > $${
(sample.statement.where.compareTo as CompareValueNode).parameterIndexes[0]! + 1
} ORDER BY "${
(sample.statement.order[0]!.orderBy as AbstractQueryFieldNodePrimitive).field
}" ASC LIMIT $1 OFFSET $2;`,
parameters: sample.statement.parameters,
Expand Down
5 changes: 3 additions & 2 deletions packages/data-driver-postgres/src/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@ import { select } from './select.js';
import { from } from './from.js';
import { limit } from './limit.js';
import { offset } from './offset.js';
import { where } from './where.js';
import { orderBy } from './orderBy.js';

/**
* Constructs an actual PostgreSQL query statement from a given abstract SQL query.
*
* @remarks
* To create a PostgreSQL statement each part is constructed in a separate function.
* In those functions it will be checked if the part should actually be created.
* In those functions it will be checked if the query part is actually needed.
* If not, the functions return null.
*
* @param query - The abstract SQL statement
* @returns An actual SQL query with parameters
*/
export function constructSqlQuery(query: AbstractSqlQuery): ParameterizedSQLStatement {
const statementParts = [select, from, orderBy, limit, offset];
const statementParts = [select, from, where, orderBy, limit, offset];

const statement = `${statementParts
.map((part) => part(query))
Expand Down
281 changes: 281 additions & 0 deletions packages/data-driver-postgres/src/query/where.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import type { AbstractSqlQuery, AbstractSqlQueryWhereConditionNode, CompareValueNode } from '@directus/data-sql';
import { beforeEach, describe, expect, test } from 'vitest';
import { where, getComparison } from './where.js';
import { randomIdentifier, randomInteger } from '@directus/random';

let sample: {
statement: AbstractSqlQuery;
};

describe('Where clause:', () => {
beforeEach(() => {
sample = {
statement: {
select: [
{
type: 'primitive',
column: randomIdentifier(),
table: randomIdentifier(),
as: randomIdentifier(),
},
],
from: randomIdentifier(),
where: {
type: 'condition',
operation: 'gt',
negate: false,
target: {
type: 'primitive',
column: randomIdentifier(),
table: randomIdentifier(),
},
compareTo: {
type: 'value',
parameterIndexes: [0],
},
},
parameters: [randomInteger(1, 10)],
},
};
});

test('Where clause', () => {
expect(where(sample.statement)).toStrictEqual(
`WHERE "${(sample.statement.where as AbstractSqlQueryWhereConditionNode).target.table}"."${
(sample.statement.where as AbstractSqlQueryWhereConditionNode).target.column
}" > $${
((sample.statement.where as AbstractSqlQueryWhereConditionNode).compareTo as CompareValueNode)
.parameterIndexes[0]! + 1
}`
);
});

test('Where clause with negation', () => {
sample.statement.where!.negate = true;

expect(where(sample.statement)).toStrictEqual(
`WHERE "${(sample.statement.where as AbstractSqlQueryWhereConditionNode).target.table}"."${
(sample.statement.where as AbstractSqlQueryWhereConditionNode).target.column
}" <= $${
((sample.statement.where as AbstractSqlQueryWhereConditionNode).compareTo as CompareValueNode)
.parameterIndexes[0]! + 1
}`
);
});
});

describe('Where clause operator mapping and parameter index insertion: ', () => {
let compareTo: CompareValueNode;

beforeEach(() => {
compareTo = {
type: 'value',
parameterIndexes: [randomInteger(1, 10)],
};
});

test('eq', () => {
expect(getComparison('eq', compareTo)).toStrictEqual(`= $${compareTo.parameterIndexes[0]! + 1}`);
});

test('gt', () => {
expect(getComparison('gt', compareTo)).toStrictEqual(`> $${compareTo.parameterIndexes[0]! + 1}`);
});

test('gte', () => {
expect(getComparison('gte', compareTo)).toStrictEqual(`>= $${compareTo.parameterIndexes[0]! + 1}`);
});

test('lt', () => {
expect(getComparison('lt', compareTo)).toStrictEqual(`< $${compareTo.parameterIndexes[0]! + 1}`);
});

test('lte', () => {
expect(getComparison('lte', compareTo)).toStrictEqual(`<= $${compareTo.parameterIndexes[0]! + 1}`);
});

test('contains', () => {
expect(getComparison('contains', compareTo)).toStrictEqual(`LIKE '%$${compareTo.parameterIndexes[0]! + 1}%'`);
});

test('starts_with', () => {
expect(getComparison('starts_with', compareTo)).toStrictEqual(`LIKE '$${compareTo.parameterIndexes[0]! + 1}%'`);
});

test('ends_with', () => {
expect(getComparison('ends_with', compareTo)).toStrictEqual(`LIKE '%$${compareTo.parameterIndexes[0]! + 1}'`);
});

test('in', () => {
compareTo = {
type: 'value',
parameterIndexes: [randomInteger(1, 10), randomInteger(1, 10)],
};

expect(getComparison('in', compareTo)).toStrictEqual(
`IN ($${compareTo.parameterIndexes[0]! + 1}, $${compareTo.parameterIndexes[1]! + 1})`
);
});
});

test('Convert filter with logical', () => {
const randomTable = randomIdentifier();
const randomColumn = randomIdentifier();

const firstColumn = randomIdentifier();
const secondColumn = randomIdentifier();
const firstValue = randomInteger(1, 100);
const secondValue = randomInteger(1, 100);

const statement: AbstractSqlQuery = {
select: [
{
type: 'primitive',
table: randomTable,
column: randomColumn,
},
],
from: randomTable,
where: {
type: 'logical',
operator: 'or',
negate: false,
childNodes: [
{
type: 'condition',
negate: false,
target: {
type: 'primitive',
table: randomTable,
column: firstColumn,
},
operation: 'gt',
compareTo: {
type: 'value',
parameterIndexes: [0],
},
},
{
type: 'condition',
negate: false,
target: {
type: 'primitive',
table: randomTable,
column: secondColumn,
},
operation: 'eq',
compareTo: {
type: 'value',
parameterIndexes: [1],
},
},
],
},
parameters: [firstValue, secondValue],
};

expect(where(statement)).toStrictEqual(
`WHERE "${randomTable}"."${firstColumn}" > $1 OR "${randomTable}"."${secondColumn}" = $2`
);
});

test('Convert filter nested and with negation', () => {
const randomTable = randomIdentifier();
const randomColumn = randomIdentifier();

const firstColumn = randomIdentifier();
const secondColumn = randomIdentifier();
const thirdColumn = randomIdentifier();
const fourthColumn = randomIdentifier();

const firstValue = randomInteger(1, 100);
const secondValue = randomInteger(1, 100);
const thirdValue = randomInteger(1, 100);
const fourthValue = randomInteger(1, 100);

const statement: AbstractSqlQuery = {
select: [
{
type: 'primitive',
table: randomTable,
column: randomColumn,
},
],
from: randomTable,
where: {
type: 'logical',
operator: 'or',
negate: false,
childNodes: [
{
type: 'condition',
negate: false,
target: {
type: 'primitive',
table: randomTable,
column: firstColumn,
},
operation: 'gt',
compareTo: {
type: 'value',
parameterIndexes: [0],
},
},
{
type: 'condition',
negate: true,
target: {
type: 'primitive',
table: randomTable,
column: secondColumn,
},
operation: 'eq',
compareTo: {
type: 'value',
parameterIndexes: [1],
},
},
{
type: 'logical',
operator: 'and',
negate: true,
childNodes: [
{
type: 'condition',
negate: true,
target: {
type: 'primitive',
table: randomTable,
column: thirdColumn,
},
operation: 'lt',
compareTo: {
type: 'value',
parameterIndexes: [2],
},
},
{
type: 'condition',
negate: false,
target: {
type: 'primitive',
table: randomTable,
column: fourthColumn,
},
operation: 'eq',
compareTo: {
type: 'value',
parameterIndexes: [3],
},
},
],
},
],
},
parameters: [firstValue, secondValue, thirdValue, fourthValue],
};

expect(where(statement)).toStrictEqual(
`WHERE "${randomTable}"."${firstColumn}" > $1 OR "${randomTable}"."${secondColumn}" != $2 OR NOT ("${randomTable}"."${thirdColumn}" >= $3 AND "${randomTable}"."${fourthColumn}" = $4)`
);
});
Loading

0 comments on commit 84bd5d0

Please sign in to comment.