Let's say that we have the following business problem we want to solve. In hypothetical application we want to allow user to define some kind of filter where he/she can define boolean logical expressions like AND/OR/NOT.
You can find UI/UX concept from DevExpress Filter editor demo page.
This tree structure completely conform logical operator priorities definition and allow us to define arbitrary logical structure.
It can be rewritten to for example SQL WHERE statement like:
WHERE Inventory <> 0 AND (ProductCode LIKE '%HD%' OR Cost < 200.0)We can also have more light UI/UX which is, in contract, unable to express full boolean logic operators priorities.
It can be rewritten to for example SQL WHERE statement like:
WHERE Inventory <> 0 OR NOT ProductCode LIKE '%HD%' AND Cost < 200.0Below we also show how to define another type of boolean expression where we only know true/false values of various
items.
WHERE NOT ItemAAA AND ItemBBB OR NOT ItemCCCWhen we explicitly add parentheses to show operator priorities result would be like this:
WHERE ((NOT ItemAAA) AND ItemBBB) OR (NOT ItemCCC)From observation of how simple UI works, we can see that we are unable to define there expression like following WHERE clause. We can change operator priorities only by using parentheses, and we are not able to define them here. We can define parentheses only in UI tree filter as described at beginning of this description.
WHERE ItemAAA AND (ItemBBB OR ItemCCC)What is purpose of this repository:
- Show how to expose REST API of simple-ui
- Persist expression with arbitrary structure to DB using MSSQL hierarchyid data type
- Convert fact expressions (like
A > 10 AND X = 'foo') to SQL WHERE clause
Note: Before starting Spring Boot's application, start MSSQL DB by running docker compose up command.
Whe we have graph like or tree like structures, and we want to persist them into relation database, one of the options is to create recursive DB tables structure and save data there.
One of the interesting features of MSSQL and ability to save data to tree like structure is to use hierarchyid data type.
You can read documentation but usage is pretty simple and following example shows how it works.
CREATE TABLE SimpleDemo
(
Level hierarchyid NOT NULL,
Location nvarchar(30) NOT NULL,
LocationType nvarchar(9) NULL
);
SELECT CAST(Level AS nvarchar(100)) AS [Converted Level], *
FROM SimpleDemo
ORDER BY Level; When we have sample data int SimpleDemo table, result of query above can be following.
Converted Level Level Location LocationType
/1/ 0x58 Europe Continent
/1/1/ 0x5AC0 France Country
/1/1/1/ 0x5AD6 Paris City
/1/2/ 0x5B40 Spain Country
/1/2/1/ 0x5B56 Madrid City
/2/ 0x68 South America Continent
/2/1/ 0x6AC0 Brazil Country
/2/1/1/ 0x6AD6 Brasilia City
/2/1/2/ 0x6ADA Bahia State
/2/1/2/1/ 0x6ADAB0 Salvador City
/3/ 0x78 Antarctica Continent
/3/1/ 0x7AC0 McMurdo Station City
Because we have on one side REST API for simple-ui and on other side we have DB table with hierarchyid column, we need
some intermediate structure that can be easily used to query, create, and convert.
We should not try to reinvent our own new concept, but we can utilise AST aka Abstract Syntax Tree data structure.
Our simple grammar and tree can be described as follows.
Expression
: NOT expression # NotExpression
| left=expression AND right=expression # AndExpression
| left=expression OR right=expression # OrExpression
| TERM # TerminalExpression
;
Using ExpressionVisitor class implementations we can easily process this tree structure
using Visitor design pattern.
Because we use simple-ui approach described above we are forced to convert between AST with proper logical operator priorities defined and between simple REST API JSON array.
Let's take a look how flat REST API looks like.
array
: item+
;
item:
{
"leftOperator": NOT | null
"value": ...
"rightOperator": AND | OR
| null (if last item of array)
}
JSON example:
[
{
"leftOperator": null,
"value": 123,
"rightOperator": "AND"
},
{
"leftOperator": "NOT",
"value": 456,
"rightOperator": "OR"
},
{
"leftOperator": null,
"value": 789,
"rightOperator": null
}
]For conversion from flat JSON array to AST we use RestDtoToExpressionConverter class.
First it takes JSON array objects with definition of leftOperator and rightOperator and creates Boolean logic
string
that contains operators AND, OR, and NOT without explicit operator precedence defined by parentheses.
Values are replaced with Identifier definitions that are unique and can be parsed by ANTLR4 defined grammar
(see ./src/main/antlr4/SimpleBoolean.g4 file).
Example above is converted to string X1 AND NOT X2 OR X3.
Second ANTLR4 generated parser is used to parse string above and StringToExpressionParser is used to convert
ANTLR4 AST to our internal Expression AST.
We use VariableExpression terminal nodes to identify items in flat JSON array.
Lastly we use RestDtoReplaceVariablesVisitor to replace variables (like X1, X2, X3, etc) in terminal nodes
with actual values from JSON array.
For conversion from AST expression to flat JSON array we use ExpressionToRestDtoConverter class.
First it uses FlattenExpressionVisitor to create list of string tokens (it contains AND/OR/NOT and
identifier) and index between generate identifier and value from terminal node.
Let's say we have following AST:
123 AND 456 OR NOT 789
(OR
(AND
123
456
)
(NOT
789
)
)
It is translated to list of tokens:
["[X1]", "AND", "[X2]", "OR", "NOT", "[X3]"]
Note: Token have [ and ] suffix/prefix for us to distinguish between operators and token identifiers.
It also creates index map:
{
"X1": 123,
"X2": 456,
"X3": 789
}
Second we find identifiers indexes in token list. They will be items in flat JSON array.
Lastly we iterate this identifiers index and at every position we
- look behind current identifier token - to see if there is
NOToperator defined - look ahead current identifier token - to see if there is
ANDorORoperator defined.
From this information we create flat JSON array items with leftOperator and rigthOperator JSON properties defined.
Result of example above would be:
[
{
"leftOperator": null,
"value": 123,
"rightOperator": "AND"
},
{
"leftOperator": null,
"value": 456,
"rightOperator": "OR"
},
{
"leftOperator": "NOT",
"value": 789,
"rightOperator": null
}
]REST API Features:
- REST Endpoint for create/update/get
Factentity - REST Endpoint for create/update/get
Identity - Convert REST DTO flat structure to
ExpressionAST- see
RestDtoToExpressionConverterclass - see
FlattenExpressionVisitorclass
- see
- Convert
ExpressionAST to REST DTO flat structure- see
ExpressionToRestDtoConverterclass - see
RestDtoReplaceVariablesVisitorclass
- see
DB features:
- DB Repository for working with
MSSQL hierarchyid
data type
- see
FactExpressionRepositoryclass - see
IdExpressionRepositoryclass
- see
- Convert DB entity to
ExpressionAST- see
DefinitionToExpressionConvertorbase class
- see
- Convert
ExpressionAST to DB entity- see
ExpressionToDefinitionConverterbase class
- see
- Convert
Factexpression AST to SQL WHERE statement using JSqlParser- see
SqlGeneratorVisitorclass
- see
DB Structure Fact rich data:
create table foo.EXPRESSION_FACT_ITEM
(
ID int identity constraint PK_EXPRESSION_FACT_ITEM primary key,
PARENT_ID int not null,
HIERARCHY hierarchyid not null,
TYPE varchar(14) not null,
LOGICAL_OPERATOR varchar(8),
VAL_COLUMN_NAME varchar(64),
VAL_OPERATOR varchar(64),
VAL_VALUE clob
)DB Structure Id simple int artificial relation:
create table foo.EXPRESSION_ID_ITEM
(
ID int identity constraint PK_EXPRESSION_ID_ITEM primary key,
PARENT_ID int not null,
HIERARCHY hierarchyid not null,
TYPE varchar(14) not null,
LOGICAL_OPERATOR varchar(8),
VAL_ID int
)Other features:
- Parse logical expression string without parentheses to detect implicit operator precedence
- see
StringToExpressionParserclass - ANTLR grammar
SimpleBoolean.g4
- see
- Integration tests using Testcontainers
- using MSSQL dockerized DB
- and Liquibase for DB migration



