Skip to content

Commit

Permalink
more filter patterns fitted (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
vogievetsky authored Jan 10, 2025
1 parent 26e8687 commit 740a5f6
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-days-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'druid-query-toolkit': patch
---

More filter patterns supported
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions src/filter-pattern/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export interface FilterPatternDefinition<P> {

export function extractOuterNot(ex: SqlExpression): [boolean, SqlExpression] {
let negated = false;
if (ex instanceof SqlUnary && ex.op === 'NOT') {
negated = true;
while (ex instanceof SqlUnary && ex.op === 'NOT') {
negated = !negated;
ex = ex.argument.changeParens([]);
}
return [negated, ex];
Expand Down Expand Up @@ -55,3 +55,7 @@ export function unwrapCastAsVarchar(ex: SqlExpression): SqlExpression {
export function oneOf<T>(thing: T, ...options: T[]): boolean {
return options.includes(thing);
}

export function xor(a: unknown, b: unknown): boolean {
return Boolean(a ? !b : b);
}
62 changes: 49 additions & 13 deletions src/filter-pattern/pattern-values.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,86 @@ import type { ValuesFilterPattern } from './pattern-values';
import { VALUES_PATTERN_DEFINITION } from './pattern-values';

describe('pattern-values', () => {
const expectations: { expression: string; pattern: ValuesFilterPattern }[] = [
const expectations: {
fixedPoint: string;
pattern: ValuesFilterPattern;
otherForms?: string[];
}[] = [
{
expression: '"DIM:cityName" IS NULL',
fixedPoint: `"cityName" = 'Paris'`,
pattern: {
type: 'values',
negated: false,
column: 'DIM:cityName',
column: 'cityName',
values: ['Paris'],
},
otherForms: [
`'Paris' = "cityName"`,
`"cityName" IN ('Paris')`,
`NOT(NOT("cityName" = 'Paris'))`,
],
},
{
fixedPoint: `"cityName" <> 'Paris'`,
pattern: {
type: 'values',
negated: true,
column: 'cityName',
values: ['Paris'],
},
otherForms: [`'Paris' <> "cityName"`, `"cityName" NOT IN ('Paris')`],
},
{
fixedPoint: '"cityName" IS NULL',
pattern: {
type: 'values',
negated: false,
column: 'cityName',
values: [null],
},
},
{
expression: '"DIM:cityName" IS NOT NULL',
fixedPoint: '"cityName" IS NOT NULL',
pattern: {
type: 'values',
negated: true,
column: 'DIM:cityName',
column: 'cityName',
values: [null],
},
},
{
expression: `("DIM:cityName" IS NULL OR "DIM:cityName" IN ('Paris', 'Marseille'))`,
fixedPoint: `("cityName" IS NULL OR "cityName" IN ('Paris', 'Marseille'))`,
pattern: {
type: 'values',
negated: false,
column: 'DIM:cityName',
column: 'cityName',
values: [null, 'Paris', 'Marseille'],
},
otherForms: [`"cityName" IS NULL OR "cityName" = 'Paris' OR "cityName" = 'Marseille'`],
},
{
expression: `("DIM:cityName" IS NOT NULL AND "DIM:cityName" NOT IN ('Paris', 'Marseille'))`,
fixedPoint: `("cityName" IS NOT NULL AND "cityName" NOT IN ('Paris', 'Marseille'))`,
pattern: {
type: 'values',
negated: true,
column: 'DIM:cityName',
column: 'cityName',
values: [null, 'Paris', 'Marseille'],
},
otherForms: [
`"cityName" IS NOT NULL AND "cityName" <> 'Paris' AND "cityName" <> 'Marseille'`,
],
},
];

describe('fit <-> toExpression', () => {
expectations.forEach(({ expression, pattern }) => {
it(`works with ${expression}`, () => {
expect(VALUES_PATTERN_DEFINITION.fit(SqlExpression.parse(expression))).toEqual(pattern);
expect(VALUES_PATTERN_DEFINITION.toExpression(pattern).toString()).toEqual(expression);
expectations.forEach(({ fixedPoint, pattern, otherForms }) => {
it(`works with ${fixedPoint}`, () => {
expect(VALUES_PATTERN_DEFINITION.fit(SqlExpression.parse(fixedPoint))).toEqual(pattern);
expect(VALUES_PATTERN_DEFINITION.toExpression(pattern).toString()).toEqual(fixedPoint);

(otherForms || []).forEach(otherForm => {
expect(VALUES_PATTERN_DEFINITION.fit(SqlExpression.parse(otherForm))).toEqual(pattern);
});
});
});
});
Expand Down
15 changes: 11 additions & 4 deletions src/filter-pattern/pattern-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
import { C } from '../shortcuts';
import type { LiteralValue, SqlExpression } from '../sql';
import { SqlColumn, SqlComparison, SqlLiteral, SqlMulti, SqlPlaceholder, SqlRecord } from '../sql';
import { filterMap } from '../utils';

import type { FilterPatternDefinition } from './common';
import { extractOuterNot, sqlRecordGetLiteralValues } from './common';
import { xor } from './utils';
import { extractOuterNot, sqlRecordGetLiteralValues, xor } from './common';

export interface ValuesFilterPattern {
type: 'values';
Expand All @@ -33,7 +33,7 @@ export const VALUES_PATTERN_DEFINITION: FilterPatternDefinition<ValuesFilterPatt

if (ex instanceof SqlMulti && (ex.op === 'OR' || ex.op === 'AND')) {
const args = ex.getArgArray();
const patterns = args.map(VALUES_PATTERN_DEFINITION.fit).filter(Boolean);
const patterns = filterMap(args, VALUES_PATTERN_DEFINITION.fit);

const pattern = patterns[0];
if (pattern && args.length === patterns.length) {
Expand Down Expand Up @@ -71,6 +71,13 @@ export const VALUES_PATTERN_DEFINITION: FilterPatternDefinition<ValuesFilterPatt
column: lhs.getName(),
values: [rhs.value],
};
} else if (lhs instanceof SqlLiteral && rhs instanceof SqlColumn) {
return {
type: 'values',
negated: xor(op === '<>', negated),
column: rhs.getName(),
values: [lhs.value],
};
}
return;

Expand Down Expand Up @@ -109,7 +116,7 @@ export const VALUES_PATTERN_DEFINITION: FilterPatternDefinition<ValuesFilterPatt
return Array.isArray(pattern.values) && Boolean(pattern.values.length);
},
toExpression({ column, values, negated }): SqlExpression {
if (!values.length) return SqlLiteral.TRUE;
if (!values.length) return SqlLiteral.FALSE;

return C(column).apply(ex => {
if (values.length === 1) {
Expand Down
22 changes: 0 additions & 22 deletions src/filter-pattern/utils.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/sql/parser/druidsql.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ BaseType =
/ SqlValues
/ SqlLiteral
/ SqlColumn
/ SqlRecordMaybe
/ SqlRecordOrExpressionInParens
/ SqlQueryInParens

//--------------------------------------------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -1477,7 +1477,7 @@ ReturningSeparator = left:_ separator:ReturningToken right:_
});
}

SqlRecordMaybe = record:SqlRecord
SqlRecordOrExpressionInParens = record:SqlRecord
{
return record.unwrapIfSingleton();
}
Expand Down
27 changes: 13 additions & 14 deletions src/sql/sql-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import { cleanObject, dedupe, objectMap } from '../utils';

import type { SeparatedArray } from '.';
import { SqlColumn, SqlFunction, SqlLiteral, SqlMulti, SqlTable } from '.';
import { indentLines, SqlColumn, SqlFunction, SqlLiteral, SqlMulti, SqlTable } from '.';
import { parse as parseSql } from './parser';
import { INDENT, NEWLINE, SPACE } from './utils';
import { NEWLINE, SPACE } from './utils';

export interface Parens {
leftSpacing?: string;
Expand Down Expand Up @@ -305,9 +305,6 @@ export abstract class SqlBase {
}
if (ret !== v) {
changed = true;
// if (ret.type !== v.type) {
// throw new Error(`can not change type of array (from ${v.type} to ${ret.type})`);
// }
}
return ret;
});
Expand Down Expand Up @@ -443,7 +440,7 @@ export abstract class SqlBase {
}

public getSpace(name: SpaceName, defaultSpace = SPACE) {
const s = this.spacing[name];
const s = this.spacing[name] as string | undefined;
return typeof s === 'string' ? s : defaultSpace;
}

Expand Down Expand Up @@ -491,16 +488,18 @@ export abstract class SqlBase {
const { parens } = this;
let str = this._toRawString();
if (parens) {
const isMultiline = str.includes(NEWLINE);
for (const paren of parens) {
let { leftSpacing, rightSpacing } = paren;
if (typeof leftSpacing === 'undefined' && typeof rightSpacing === 'undefined') {
const lines = str.split('\n');
if (lines.length > 1) {
leftSpacing = rightSpacing = '\n';
str = lines.map(l => INDENT + l).join(NEWLINE);
}
const { leftSpacing, rightSpacing } = paren;
if (
typeof leftSpacing === 'undefined' &&
typeof rightSpacing === 'undefined' &&
isMultiline
) {
str = `(${NEWLINE}${indentLines(str)}${NEWLINE})`;
} else {
str = `(${leftSpacing || ''}${str}${rightSpacing || ''})`;
}
str = `(${leftSpacing || ''}${str}${rightSpacing || ''})`;
}
}
return this.getSpace('initial', '') + str + this.getSpace('final', '');
Expand Down
7 changes: 7 additions & 0 deletions src/sql/utils/general/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export const NEWLINE = '\n';
export const INDENT = ' ';
export const NEWLINE_INDENT = NEWLINE + INDENT;

export function indentLines(str: string): string {
return str
.split(NEWLINE)
.map(l => INDENT + l)
.join(NEWLINE);
}

export function trimString(str: string, maxLength: number): string {
if (str.length < maxLength) return str;
return str.substr(0, Math.max(maxLength - 3, 1)) + '...';
Expand Down

0 comments on commit 740a5f6

Please sign in to comment.