Skip to content

Commit

Permalink
Merge pull request #643 from contember/feat/ql-quantifiers
Browse files Browse the repository at this point in the history
query language quantifiers
  • Loading branch information
matej21 authored Nov 9, 2023
2 parents 7379452 + f5cc6c7 commit fed1c5a
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 18 deletions.
2 changes: 2 additions & 0 deletions build/api/binding.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,8 @@ export namespace Parser {
export type EntryPoint = keyof ParserResult;
// (undocumented)
export interface ParserResult {
// (undocumented)
columnValue: AST.ColumnValue;
// (undocumented)
filter: Filter;
// (undocumented)
Expand Down
1 change: 1 addition & 0 deletions packages/binding/src/queryLanguage/CacheStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class CacheStore {
filter: new LRUCache(100),
orderBy: new LRUCache(50),
taggedMap: new LRUCache(50),
columnValue: new LRUCache(500),
}
}
}
78 changes: 61 additions & 17 deletions packages/binding/src/queryLanguage/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { EmbeddedActionsParser, Lexer } from 'chevrotain'
import { Environment } from '../dao'
import type { EntityName, FieldName, Filter, OrderBy, UniqueWhere } from '../treeParameters'
import type {
ParsedTaggedMap,
ParsedTaggedMapVariableValue,
ParsedTaggedMapLiteralValue,
ParsedTaggedMapEntry,
ParsedHasManyRelation,
ParsedHasOneRelation,
ParsedQualifiedEntityList,
Expand All @@ -15,6 +11,10 @@ import type {
ParsedRelativeEntityList,
ParsedRelativeSingleEntity,
ParsedRelativeSingleField,
ParsedTaggedMap,
ParsedTaggedMapEntry,
ParsedTaggedMapLiteralValue,
ParsedTaggedMapVariableValue,
ParsedUnconstrainedQualifiedEntityList,
ParsedUnconstrainedQualifiedSingleEntity,
} from './ParserResults'
Expand Down Expand Up @@ -317,29 +317,59 @@ class Parser extends EmbeddedActionsParser {
})

private fieldWhere: () => Parser.AST.FieldWhere = this.RULE('fieldWhere', () => {
const fields: FieldName[] = []
const fields: { name: FieldName, modifier?: string }[] = []

this.AT_LEAST_ONE_SEP({
SEP: tokens.Dot,
DEF: () => {
fields.push(this.SUBRULE(this.fieldIdentifier))
fields.push(this.SUBRULE(this.fieldIdentifierWithOptionalModifier))
},
})

const condition = this.SUBRULE(this.condition)

let i = fields.length - 1
let where: Parser.AST.FieldWhere = {
[fields[i--]]: condition,
const createFieldWhere = (value: Parser.AST.Condition | Parser.AST.FieldWhere): Parser.AST.FieldWhere => {
return this.ACTION(() => {
let val = value
for (let i = fields.length - 1; i >= 0; i--) {
const field = fields[i]
if (field.modifier === 'none') {
val = {
not: {
[field.name]: val,
},
}
} else if (field.modifier === 'all') {
val = {
not: {
[field.name]: { not: val },
},
}
} else if (!field.modifier || field.modifier === 'some') {
val = {
[field.name]: val,
}
} else {
throw new QueryLanguageError(`Unknown modifier '${field.modifier}', expected 'none', 'some' or 'all'`)
}
}
return val
})
}

while (i >= 0) {
where = {
[fields[i--]]: where,
}
}

return where
return this.OR([
{
ALT: () => {
const condition = this.SUBRULE(this.condition)
return createFieldWhere(condition)
},
},
{
ALT: () => {
const subWhere = this.SUBRULE(this.nonUniqueWhere)
return createFieldWhere(subWhere)
},
},
])
})

private condition: () => Parser.AST.Condition = this.RULE('condition', () => {
Expand Down Expand Up @@ -638,6 +668,15 @@ class Parser extends EmbeddedActionsParser {
])
})

private fieldIdentifierWithOptionalModifier: () => { name: FieldName, modifier?: string } = this.RULE('fieldIdentifierWithOptionalModifier', () => {
const fieldIdentifier = this.SUBRULE1(this.fieldIdentifier)
const modifier = this.OPTION(() => {
this.CONSUME(tokens.Colon)
return this.SUBRULE(this.identifier)
})
return { name: fieldIdentifier, modifier }
})

private fieldIdentifier: () => FieldName = this.RULE('fieldIdentifier', () => {
return this.SUBRULE(this.identifier)
// return this.OR([
Expand Down Expand Up @@ -705,6 +744,7 @@ class Parser extends EmbeddedActionsParser {
return image
.substring(1, image.length - 1)
.replace("\\'", "'")
.replace(`\\"`, `"`)
.replace('\\b', '\b')
.replace('\\f', '\f')
.replace('\\n', '\n')
Expand Down Expand Up @@ -824,6 +864,9 @@ class Parser extends EmbeddedActionsParser {
case 'taggedMap':
expression = Parser.parser.taggedMap()
break
case 'columnValue':
expression = Parser.parser.columnValue()
break
default:
throw new QueryLanguageError(`Not implemented entry point '${entry}'`)
}
Expand Down Expand Up @@ -864,6 +907,7 @@ namespace Parser {
filter: Filter // E.g. [author.son.age < 123]
orderBy: OrderBy // E.g. items.order asc, items.content.name asc
taggedMap: ParsedTaggedMap // E.g editUser(id: $entity.id, foo: 'bar')
columnValue: AST.ColumnValue
}

export type EntryPoint = keyof ParserResult
Expand Down
2 changes: 1 addition & 1 deletion packages/binding/src/queryLanguage/tokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const tokens = {

StringLiteral: createToken({
name: 'StringLiteral',
pattern: /'(:?[^\\']|\\(:?[bfnrtv'\\/]|u[0-9a-fA-F]{4}))*'/,
pattern: /'(:?[^\\']|\\(:?[bfnrtv"'\\/]|u[0-9a-fA-F]{4}))*'|"(:?[^\\"]|\\(:?[bfnrtv"'\\/]|u[0-9a-fA-F]{4}))*"/,
}),

LeftParenthesis: createToken({
Expand Down
45 changes: 45 additions & 0 deletions packages/binding/tests/cases/unit/queryLanguage/filter.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,49 @@ describe('filter QueryLanguage parser', () => {
and: [{ a: { b: { c: { lt: 123 } } } }, { a: { d: { eq: 456 } } }],
})
})

it('should parse conditions with sub-filters', () => {
expect(parse(`[tags[name = 'foo' && isPublished = true]]`)).toEqual({
tags: {
and: [
{ name: { eq: 'foo' } },
{ isPublished: { eq: true } },
],
},
})
})

it('should parse conditions with quantifiers', () => {
expect(parse(`[tags:none[name = 'foo' && isPublished = true]]`)).toEqual({
not: {
tags: {
and: [
{ name: { eq: 'foo' } },
{ isPublished: { eq: true } },
],
},
},
})
})


it('should parse conditions with quantifiers', () => {
expect(parse(`[tags:all[isPublished = true]]`)).toEqual({
not: {
tags: {
not: { isPublished: { eq: true } },
},
},
})
})

it('should parse conditions with quantifiers without subfilter', () => {
expect(parse(`[tags:all.isPublished = true]`)).toEqual({
not: {
tags: {
not: { isPublished: { eq: true } },
},
},
})
})
})
17 changes: 17 additions & 0 deletions packages/binding/tests/cases/unit/queryLanguage/general.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ import { Environment } from '../../../../src/dao'
import { Parser } from '../../../../src/queryLanguage'

describe('query language parser', () => {
it('shoud parse column value', () => {
const env = Environment.create()
expect(Parser.parseQueryLanguageExpression('123', 'columnValue', env)).toEqual(123)
expect(Parser.parseQueryLanguageExpression('123.456', 'columnValue', env)).toEqual(123.456)
expect(Parser.parseQueryLanguageExpression('true', 'columnValue', env)).toEqual(true)
expect(Parser.parseQueryLanguageExpression('false', 'columnValue', env)).toEqual(false)
expect(Parser.parseQueryLanguageExpression('null', 'columnValue', env)).toEqual(null)
expect(Parser.parseQueryLanguageExpression("'foo'", 'columnValue', env)).toEqual('foo')
expect(Parser.parseQueryLanguageExpression(`'foo\\'bar'`, 'columnValue', env)).toEqual("foo'bar")
expect(Parser.parseQueryLanguageExpression(`'foo"bar'`, 'columnValue', env)).toEqual(`foo"bar`)
expect(Parser.parseQueryLanguageExpression(`'foo\\"bar'`, 'columnValue', env)).toEqual(`foo"bar`)
expect(Parser.parseQueryLanguageExpression('"foo"', 'columnValue', env)).toEqual('foo')
expect(Parser.parseQueryLanguageExpression(`"foo\\"bar"`, 'columnValue', env)).toEqual('foo"bar')
expect(Parser.parseQueryLanguageExpression(`"foo\\'bar"`, 'columnValue', env)).toEqual("foo'bar")
expect(Parser.parseQueryLanguageExpression(`"foo'bar"`, 'columnValue', env)).toEqual("foo'bar")
})

it('should resolve variables adhering to the principle maximal munch', () => {
const environment = Environment.create().withVariables({
ab: 456,
Expand Down

0 comments on commit fed1c5a

Please sign in to comment.