From 7301ef77642dfa54dd63f819d53986bab9960550 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Thu, 27 Feb 2020 12:28:47 +0100 Subject: [PATCH] [jOOQ/jOOQ#9888] Support parsing some PostgreSQL specific operators --- .../main/java/org/jooq/impl/ParserImpl.java | 132 +++++++++++++----- 1 file changed, 99 insertions(+), 33 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java index a69c04ce95..2e3a2ee4ea 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/ParserImpl.java @@ -4616,6 +4616,7 @@ final class ParserImpl implements Parser { Comparator comp; TSQLOuterJoinComparator outer; boolean not; + boolean notOp = false; left = parseConcat(ctx, null); not = parseKeywordIf(ctx, "NOT"); @@ -4749,13 +4750,13 @@ final class ParserImpl implements Parser { ? ((Field) left).between((Field) r1, (Field) r2) : new RowBetweenCondition((Row) left, (Row) r1, not, symmetric, (Row) r2); } - else if (left instanceof Field && parseKeywordIf(ctx, "LIKE")) { + else if (left instanceof Field && (parseKeywordIf(ctx, "LIKE") || parseOperatorIf(ctx, "~~") || (notOp = parseOperatorIf(ctx, "!~~")))) { if (parseKeywordIf(ctx, "ANY")) { parse(ctx, '('); if (peekKeyword(ctx, "SELECT") || peekKeyword(ctx, "SEL") || peekKeyword(ctx, "WITH")) { Select select = parseWithOrSelect(ctx); parse(ctx, ')'); - LikeEscapeStep result = not ? ((Field) left).notLike(any(select)) : ((Field) left).like(any(select)); + LikeEscapeStep result = (not ^ notOp) ? ((Field) left).notLike(any(select)) : ((Field) left).like(any(select)); return parseEscapeClauseIf(ctx, result); } else { @@ -4771,7 +4772,7 @@ final class ParserImpl implements Parser { parse(ctx, ')'); } Field[] fieldArray = fields.toArray(new Field[0]); - LikeEscapeStep result = not ? ((Field) left).notLike(any(fieldArray)) : ((Field) left).like(any(fieldArray)); + LikeEscapeStep result = (not ^ notOp) ? ((Field) left).notLike(any(fieldArray)) : ((Field) left).like(any(fieldArray)); return parseEscapeClauseIf(ctx, result); } } @@ -4780,7 +4781,7 @@ final class ParserImpl implements Parser { if (peekKeyword(ctx, "SELECT") || peekKeyword(ctx, "SEL") || peekKeyword(ctx, "WITH")) { Select select = parseWithOrSelect(ctx); parse(ctx, ')'); - LikeEscapeStep result = not ? ((Field) left).notLike(all(select)) : ((Field) left).like(all(select)); + LikeEscapeStep result = (not ^ notOp) ? ((Field) left).notLike(all(select)) : ((Field) left).like(all(select)); return parseEscapeClauseIf(ctx, result); } else { @@ -4796,26 +4797,28 @@ final class ParserImpl implements Parser { parse(ctx, ')'); } Field[] fieldArray = fields.toArray(new Field[0]); - LikeEscapeStep result = not ? ((Field) left).notLike(all(fieldArray)) : ((Field) left).like(all(fieldArray)); + LikeEscapeStep result = (not ^ notOp) ? ((Field) left).notLike(all(fieldArray)) : ((Field) left).like(all(fieldArray)); return parseEscapeClauseIf(ctx, result); } } else { Field right = toField(ctx, parseConcat(ctx, null)); - LikeEscapeStep like = not ? ((Field) left).notLike(right) : ((Field) left).like(right); + LikeEscapeStep like = (not ^ notOp) ? ((Field) left).notLike(right) : ((Field) left).like(right); return parseEscapeClauseIf(ctx, like); } } - else if (left instanceof Field && parseKeywordIf(ctx, "ILIKE")) { + else if (left instanceof Field && (parseKeywordIf(ctx, "ILIKE") || parseOperatorIf(ctx, "~~*") || (notOp = parseOperatorIf(ctx, "!~~*")))) { Field right = toField(ctx, parseConcat(ctx, null)); - LikeEscapeStep like = not ? ((Field) left).notLikeIgnoreCase(right) : ((Field) left).likeIgnoreCase(right); + LikeEscapeStep like = (not ^ notOp) ? ((Field) left).notLikeIgnoreCase(right) : ((Field) left).likeIgnoreCase(right); return parseEscapeClauseIf(ctx, like); } else if (left instanceof Field && (parseKeywordIf(ctx, "REGEXP") || parseKeywordIf(ctx, "RLIKE") - || parseKeywordIf(ctx, "LIKE_REGEX"))) { + || parseKeywordIf(ctx, "LIKE_REGEX") + || parseOperatorIf(ctx, "~") + || (notOp = parseOperatorIf(ctx, "!~")))) { Field right = toField(ctx, parseConcat(ctx, null)); - return not + return (not ^ notOp) ? ((Field) left).notLikeRegex(right) : ((Field) left).likeRegex(right); } @@ -10532,6 +10535,46 @@ final class ParserImpl implements Parser { return peekKeyword(ctx, string, true, false, true); } + private static final boolean parseOperator(ParserContext ctx, String operator) { + if (!parseOperatorIf(ctx, operator)) + throw ctx.expected("Operator '" + operator + "'"); + + return true; + } + + private static final boolean parseOperatorIf(ParserContext ctx, String operator) { + return peekOperator(ctx, operator, true); + } + + private static final boolean peekOperator(ParserContext ctx, String operator) { + return peekOperator(ctx, operator, false); + } + + private static final boolean peekOperator(ParserContext ctx, String operator, boolean updatePosition) { + int length = operator.length(); + int position = ctx.position(); + + if (ctx.sql.length < position + length) + return false; + + int pos = afterWhitespace(ctx, position, false); + + for (int i = 0; i < length; i++, pos++) + if (ctx.sql[pos] != operator.charAt(i)) + return false; + + // [#9888] An operator that is followed by a special character is very likely another, more complex operator + if (ctx.isOperatorPart(pos)) + return false; + + if (updatePosition) { + ctx.position(pos); + parseWhitespaceIf(ctx); + } + + return true; + } + private static final boolean parseKeyword(ParserContext ctx, String keyword) { if (!parseKeywordIf(ctx, keyword)) throw ctx.expected("Keyword '" + keyword + "'"); @@ -10605,34 +10648,12 @@ final class ParserImpl implements Parser { private static final boolean peekKeyword(ParserContext ctx, String keyword, boolean updatePosition, boolean peekIntoParens, boolean requireFunction) { int length = keyword.length(); - int skip; int position = ctx.position(); if (ctx.sql.length < position + length) return false; - skipLoop: - for (skip = 0; position + skip < ctx.sql.length; skip++) { - char c = ctx.sql[position + skip]; - - switch (c) { - case ' ': - case '\t': - case '\r': - case '\n': - continue skipLoop; - - case '(': - if (peekIntoParens) - continue skipLoop; - else - break skipLoop; - - default: - break skipLoop; - } - } - + int skip = afterWhitespace(ctx, position, peekIntoParens) - position; for (int i = 0; i < length; i++) { char c = keyword.charAt(i); @@ -10676,6 +10697,10 @@ final class ParserImpl implements Parser { } private static final int afterWhitespace(ParserContext ctx, int position) { + return afterWhitespace(ctx, position, false); + } + + private static final int afterWhitespace(ParserContext ctx, int position, boolean peekIntoParens) { // [#8074] The SQL standard and some implementations (e.g. PostgreSQL, // SQL Server) support nesting block comments @@ -10692,6 +10717,12 @@ final class ParserImpl implements Parser { position = i + 1; continue loop; + case '(': + if (peekIntoParens) + continue loop; + else + break loop; + case '/': if (i + 1 < ctx.sql.length && ctx.sql[i + 1] == '*') { i = i + 2; @@ -11165,6 +11196,41 @@ final class ParserContext { return Character.isWhitespace(character(pos)); } + boolean isOperatorPart() { + return isOperatorPart(character()); + } + + boolean isOperatorPart(int pos) { + return isOperatorPart(character(pos)); + } + + boolean isOperatorPart(char character) { + // Obtain all distinct, built-in PostgreSQL operator characters: + // select distinct regexp_split_to_table(oprname, '') from pg_catalog.pg_operator order by 1; + switch (character) { + case '!': + case '#': + case '%': + case '&': + case '*': + case '+': + case '-': + case '/': + case ':': + case '<': + case '=': + case '>': + case '?': + case '@': + case '^': + case '|': + case '~': + return true; + } + + return false; + } + boolean isIdentifierPart() { return isIdentifierPart(character()); }