Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lemon-hairs-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'druid-query-toolkit': minor
---

Allow parsing SET statements in an otherwise unparsable string and more integrations"
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@

/node_modules/
/coverage/
/coverage_old/
/build/
/dist/
/types/
/src/sql/parser/index.ts
/src/sql/parser/.DS_Store
CLAUDE.md

# TypeScript cache
*.tsbuildinfo

_old/
6 changes: 6 additions & 0 deletions .idea/google-java-format.xml

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

3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,13 @@ ORDER BY 5 DESC
*/
```

For more examples check out the unit tests.
For more examples, check out the unit tests.

#### ToDo

Not every valid DruidSQL construct can currently be parsed, the following snippets are not currently supported:

- `(a, b) IN (subquery)`
- Support `FROM "wikipedia_k" USING (k)`

## License

Expand Down
1 change: 1 addition & 0 deletions script/compile-peg.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ try {
parser = peg.generate(header + '\n\n' + rules, {
output: 'source',
plugins: [tspegjs],
allowedStartRules: ['Start', 'StartSetStatementsOnly'],
});
} catch (e) {
console.error('Failed to compile');
Expand Down
27 changes: 9 additions & 18 deletions src/filter-pattern/unify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ function backAndForthNotCustom(expression: string): void {
}

describe('filter-pattern', () => {
it('fixed points', () => {
const expressions: string[] = [
describe('fixed point expressions', () => {
it.each([
`"lol" = 'hello'`,
`"lol" <> 'hello'`,
`"lol" IN ('hello', 'goodbye')`,
Expand Down Expand Up @@ -67,28 +67,19 @@ describe('filter-pattern', () => {
`TIMESTAMP '2022-06-30 22:56:14.123' <= "__time" AND "__time" <= TIMESTAMP '2022-06-30 22:56:15.923'`,
`TIMESTAMP '2022-06-30 22:56:14.123' < "__time" AND "__time" <= TIMESTAMP '2022-06-30 22:56:15.923'`,
`(TIME_FLOOR(MAX_DATA_TIME(), 'P3M', NULL, 'Etc/UTC') <= "DIM:__time" AND "DIM:__time" < TIME_SHIFT(TIME_FLOOR(MAX_DATA_TIME(), 'P3M', NULL, 'Etc/UTC'), 'P1D', 1, 'Etc/UTC'))`,
];

for (const expression of expressions) {
try {
backAndForthNotCustom(expression);
} catch (e) {
console.log(`Problem with: \`${expression}\``);
throw e;
}
}
])('correctly handles expression: %s', expression => {
backAndForthNotCustom(expression);
});
});

it('invalid expressions', () => {
const expressions: string[] = [
describe('invalid expressions', () => {
it.each([
`"__time" >= TIMESTAMP '2022-06-30 22:56:15.923' AND TIMESTAMP '2021-06-30 22:56:14.123' >= "__time"`,
`TIMESTAMP '2021-06-30 22:56:14.123' >= "__time" AND "__time" >= TIMESTAMP '2022-06-30 22:56:15.923'`,
];

for (const expression of expressions) {
])('correctly handles invalid expression: %s', expression => {
const pattern = fitFilterPattern(SqlExpression.parse(expression));
expect(pattern.type).toEqual('custom');
}
});
});

describe('fitFilterPattern', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/introspect/introspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class Introspect {
}

static getQueryColumnIntrospectionQuery(query: SqlQuery | SqlTable): SqlQuery {
return SqlQuery.create(query).changeLimitValue(0);
return SqlQuery.selectStarFrom(query).changeLimitValue(0);
}

static getQueryColumnIntrospectionPayload(
Expand Down
1 change: 1 addition & 0 deletions src/sql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export * from './sql-case/sql-when-then-part';
export * from './sql-case/sql-case';
export * from './sql-alias/sql-alias';
export * from './sql-labeled-expression/sql-labeled-expression';
export * from './sql-key-value/sql-key-value';
export * from './sql-window-spec/sql-window-spec';
export * from './sql-window-spec/sql-frame-bound';
export * from './sql-set-statement/sql-set-statement';
Expand Down
82 changes: 81 additions & 1 deletion src/sql/parser/druidsql.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@
* limitations under the License.
*/

Start = initial:_? thing:(SqlQueryWithPossibleContext / SqlAlias) final:_sc?
Start = initial:_ thing:(SqlQueryWithPossibleContext / SqlAlias) final:_sc
{
if (initial) thing = thing.changeSpace('initial', initial);
if (final) thing = thing.changeSpace('final', final);
return thing;
}

StartSetStatementsOnly = spaceBefore:_ statements:(SqlSetStatement _sc)* rest:$(.*)
{
let ret = {
spaceBefore: spaceBefore,
rest: rest
}

if (statements.length) {
ret.contextStatements = new S.SeparatedArray(
statements.map(function(x) { return x[0] }),
statements.map(function(x) { return x[1] }).slice(0, statements.length - 1)
);
ret.spaceAfter = statements[statements.length - 1][1];
}

return ret;
}

// ------------------------------

SqlAlias = expression:Expression alias:((_ AsToken)? _ RefNameAlias)? columns:(_ SqlColumnList)?
Expand Down Expand Up @@ -62,6 +80,38 @@ SqlLabeledExpression = label:RefNameAlias preArrow:_ "=>" postArrow:_ expression
});
}

SqlKeyValue = LongKeyValueForm / ShortKeyValueForm

LongKeyValueForm = keyToken:KeyToken postKey:_ key:Expression postKeyExpression:_ valueToken:ValueToken preValueExpression:_ value:Expression
{
return new S.SqlKeyValue({
key: key,
value: value,
spacing: {
postKey: postKey,
postKeyExpression: postKeyExpression,
preValueExpression: preValueExpression
},
keywords: {
key: keyToken,
value: valueToken
}
});
}

ShortKeyValueForm = key:Expression postKeyExpression:_ ":" preValueExpression:_ value:Expression
{
return new S.SqlKeyValue({
key: key,
value: value,
short: true,
spacing: {
postKeyExpression: postKeyExpression,
preValueExpression: preValueExpression
}
});
}

SqlExtendClause =
extend:(ExtendToken _)?
OpenParen
Expand Down Expand Up @@ -1010,6 +1060,7 @@ Function =
/ TimestampAddDiffFunction
/ PositionFunction
/ JsonValueReturningFunction
/ JsonObjectFunction
/ ArrayFunction
/ NakedFunction

Expand Down Expand Up @@ -1171,6 +1222,32 @@ JsonValueReturningFunction =
});
}

JsonObjectFunction =
functionName:JsonObjectToken
preLeftParen:_
OpenParen
postLeftParen:_
head:SqlKeyValue?
tail:(CommaSeparator SqlKeyValue)*
postArguments:_
CloseParen
{
var value = {
functionName: makeFunctionName(functionName)
};
var spacing = value.spacing = {
preLeftParen: preLeftParen,
postLeftParen: postLeftParen
};

if (head) {
value.args = makeSeparatedArray(head, tail);
spacing.postArguments = postArguments;
}

return new S.SqlFunction(value);
}

ExtractFunction =
functionName:(ExtractToken / ('"' ExtractToken '"'))
preLeftParen:_
Expand Down Expand Up @@ -1881,6 +1958,9 @@ IntoToken = $("INTO"i !IdentifierPart)
IsToken = $("IS"i !IdentifierPart)
JoinToken = $("JOIN"i !IdentifierPart)
JsonValueToken = $("JSON_VALUE"i !IdentifierPart)
JsonObjectToken = $("JSON_OBJECT"i !IdentifierPart)
KeyToken = $("KEY"i !IdentifierPart)
ValueToken = $("VALUE"i !IdentifierPart)
LeadingToken = $("LEADING"i !IdentifierPart)
LikeToken = $("LIKE"i !IdentifierPart)
LimitToken = $("LIMIT"i !IdentifierPart)
Expand Down
46 changes: 43 additions & 3 deletions src/sql/sql-alias/sql-alias.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,49 @@ describe('SqlAlias', () => {
});

describe('.create', () => {
expect(
SqlAlias.create(SqlAlias.create(SqlColumn.create('X'), 'name1'), 'name2').toString(),
).toEqual('"X" AS "name2"');
it('overwrites existing alias when aliasing an already aliased expression', () => {
expect(
SqlAlias.create(SqlAlias.create(SqlColumn.create('X'), 'name1'), 'name2').toString(),
).toEqual('"X" AS "name2"');
});

it('creates a simple alias with string column and string alias', () => {
expect(SqlAlias.create(SqlColumn.create('col1'), 'alias1').toString()).toEqual(
'"col1" AS "alias1"',
);
});

it('creates an alias with RefName object as alias', () => {
const refName = RefName.create('myAlias', true);
expect(SqlAlias.create(SqlColumn.create('col1'), refName).toString()).toEqual(
'"col1" AS "myAlias"',
);
});

it('auto-quotes aliases that are reserved keywords', () => {
expect(SqlAlias.create(SqlColumn.create('col1'), 'select').toString()).toEqual(
'"col1" AS "select"',
);
});

it('forces quotes when forceQuotes is true', () => {
expect(SqlAlias.create(SqlColumn.create('col1'), 'normal', true).toString()).toEqual(
'"col1" AS "normal"',
);
});

it('adds parentheses to SqlQuery expressions', () => {
const query = SqlQuery.create('tbl');
const aliasedQuery = SqlAlias.create(query, 'subq');
const result = aliasedQuery.toString();

// Check that the result contains the main components rather than exact formatting
expect(result).toContain('(');
expect(result).toContain(')');
expect(result).toContain('SELECT');
expect(result).toContain('FROM "tbl"');
expect(result).toContain('AS "subq"');
});
});

describe('#changeAlias', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/sql/sql-alias/sql-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export class SqlAlias extends SqlExpression {
public getUnderlyingExpression(): SqlExpression {
return this.expression;
}

public changeUnderlyingExpression(newExpression: SqlExpression): SqlExpression {
return this.changeExpression(newExpression);
}
}

SqlBase.register(SqlAlias);
5 changes: 5 additions & 0 deletions src/sql/sql-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type SqlTypeDesignator =
| 'joinPart'
| 'alias'
| 'labeledExpression'
| 'keyValue'
| 'betweenPart'
| 'likePart'
| 'comparison'
Expand Down Expand Up @@ -119,6 +120,7 @@ export type KeywordName =
| 'into'
| 'join'
| 'joinType'
| 'key'
| 'limit'
| 'natural'
| 'offset'
Expand All @@ -144,6 +146,7 @@ export type KeywordName =
| 'unbounded'
| 'union'
| 'using'
| 'value'
| 'values'
| 'when'
| 'where'
Expand All @@ -167,6 +170,7 @@ export type SpaceName =
| 'postDecorator'
| 'postDot'
| 'postElse'
| 'postKeyExpression'
| 'postEquals'
| 'postEscape'
| 'postExplainPlanFor'
Expand Down Expand Up @@ -246,6 +250,7 @@ export type SpaceName =
| 'prePartitionedByClause'
| 'preUnion'
| 'preUsing'
| 'preValueExpression'
| 'preWhereClause';

export interface SqlBaseValue {
Expand Down
25 changes: 8 additions & 17 deletions src/sql/sql-case/sql-case.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,14 @@ import { backAndForth } from '../../test-utils';
import { SqlCase, SqlExpression } from '..';

describe('CaseExpression', () => {
it('things that work', () => {
const queries: string[] = [
`CASE WHEN (A) THEN 'hello' END`,
`CASE WHEN TIMESTAMP '2019-08-27 18:00:00'<=(t."__time") AND (t."__time")<TIMESTAMP '2019-08-28 00:00:00' THEN (t."__time") END`,
`CASE WHEN (3<="__time") THEN 1 END`,
`CASE WHEN (TIMESTAMP '2019-08-27 18:00:00'<=(t."__time") AND (t."__time")<TIMESTAMP '2019-08-28 00:00:00') THEN (t."__time") END`,
`CASE country WHEN 'United States', 'Argentina' THEN 'US' ELSE 'Blah' END`,
];

for (const sql of queries) {
try {
backAndForth(sql, SqlCase);
} catch (e) {
console.log(`Problem with: \`${sql}\``);
throw e;
}
}
it.each([
`CASE WHEN (A) THEN 'hello' END`,
`CASE WHEN TIMESTAMP '2019-08-27 18:00:00'<=(t."__time") AND (t."__time")<TIMESTAMP '2019-08-28 00:00:00' THEN (t."__time") END`,
`CASE WHEN (3<="__time") THEN 1 END`,
`CASE WHEN (TIMESTAMP '2019-08-27 18:00:00'<=(t."__time") AND (t."__time")<TIMESTAMP '2019-08-28 00:00:00') THEN (t."__time") END`,
`CASE country WHEN 'United States', 'Argentina' THEN 'US' ELSE 'Blah' END`,
])('does back and forth with %s', sql => {
backAndForth(sql, SqlCase);
});

it('caseless CASE Expression', () => {
Expand Down
1 change: 1 addition & 0 deletions src/sql/sql-case/sql-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class SqlCase extends SqlExpression {
public readonly caseExpression?: SqlExpression;
public readonly whenThenParts: SeparatedArray<SqlWhenThenPart>;
public readonly elseExpression?: SqlExpression;

constructor(options: SqlCaseValue) {
super(options, SqlCase.type);
this.caseExpression = options.caseExpression;
Expand Down
Loading