diff --git a/src/Condition.php b/src/Condition.php new file mode 100644 index 0000000..f98c0a3 --- /dev/null +++ b/src/Condition.php @@ -0,0 +1,23 @@ +isOfType(Token::TOKEN_TYPE_COMMENT, Token::TOKEN_TYPE_BLOCK_COMMENT)) { - if ($token->isOfType(Token::TOKEN_TYPE_BLOCK_COMMENT)) { - $indent = str_repeat($tab, $indentLevel); - $return = rtrim($return, " \t"); - $return .= "\n" . $indent; - $highlighted = str_replace("\n", "\n" . $indent, $highlighted); + // Always add newline after + $newline = true; + $isBlockComment = $token->isOfType(Token::TOKEN_TYPE_BLOCK_COMMENT); + $indent = str_repeat($tab, $indentLevel); + + if ($isBlockComment) { + // Remove trailing indent from previous $newline + $return = rtrim($return, $tab) . "\n" . $indent; + } else { + $prev = $cursor->subCursor()->previous(); + // Single line comment wants to have a newline before + if ($prev && ! $addedNewline && strpos($prev->value(), "\n") !== false) { + $return .= "\n" . $indent; + } elseif (! $addedNewline) { + $return .= ' '; + } + } + + if ($isBlockComment) { + $return .= str_replace("\n", "\n" . $indent, $highlighted); + continue; } $return .= $highlighted; - $newline = true; continue; } - if ($inlineParentheses) { - // End of inline parentheses - if ($token->value() === ')') { - $return = rtrim($return, ' '); + // Allow another block to finish an EOF type block + if (count($expectedBlockEnds) > 1) { + $last = end($expectedBlockEnds); + $prev = prev($expectedBlockEnds); + + if ($prev && $last->eof && ! $prev->eof && $token->isBlockEnd($prev)) { + array_pop($expectedBlockEnds); + array_pop($indentTypes); + $indentLevel--; + } + } + + $blockEndCondition = end($expectedBlockEnds); + + if ($inlineBlock) { + // End of inline block + if ($blockEndCondition && $token->isBlockEnd($blockEndCondition)) { + array_pop($expectedBlockEnds); if ($inlineIndented) { - array_shift($indentTypes); + array_pop($indentTypes); $indentLevel--; - $return = rtrim($return, ' '); $return .= "\n" . str_repeat($tab, $indentLevel); } - $inlineParentheses = false; + $inlineBlock = false; + + $return .= $highlighted; + + $nextNotWhitespace = $cursor->subCursor()->next(Token::TOKEN_TYPE_WHITESPACE); + if ($nextNotWhitespace && $nextNotWhitespace->wantsSpaceBefore()) { + $return .= ' '; + } - $return .= $highlighted . ' '; continue; } @@ -142,11 +177,13 @@ public function format(string $string, string $indentString = ' '): string $inlineCount += strlen($token->value()); } - // Opening parentheses increase the block indent level and start a new line - if ($token->value() === '(') { - // First check if this should be an inline parentheses block + $newBlockEndCondition = $token->isBlockStart(); + + // Start of new block, increase the indent level and start a new line + if ($newBlockEndCondition !== null) { + // First check if this should be an inline block // Examples are "NOW()", "COUNT(*)", "int(10)", key(`somecolumn`), DECIMAL(7,2) - // Allow up to 3 non-whitespace tokens inside inline parentheses + // Allow up to 3 non-whitespace tokens inside inline block $length = 0; $subCursor = $cursor->subCursor(); for ($j = 1; $j <= 250; $j++) { @@ -156,20 +193,12 @@ public function format(string $string, string $indentString = ' '): string break; } - // Reached closing parentheses, able to inline it - if ($next->value() === ')') { - $inlineParentheses = true; - $inlineCount = 0; - $inlineIndented = false; - break; - } - - // Reached an invalid token for inline parentheses + // Reached an invalid token for inline block if ($next->value() === ';' || $next->value() === '(') { break; } - // Reached an invalid token type for inline parentheses + // Reached an invalid token type for inline block if ( $next->isOfType( Token::TOKEN_TYPE_RESERVED_TOPLEVEL, @@ -181,10 +210,18 @@ public function format(string $string, string $indentString = ' '): string break; } + // Reached closing condition, able to inline it + if ($next->isBlockEnd($newBlockEndCondition)) { + $inlineBlock = true; + $inlineCount = 0; + $inlineIndented = false; + break; + } + $length += strlen($next->value()); } - if ($inlineParentheses && $length > 30) { + if ($inlineBlock && $length > 30) { $increaseBlockIndent = true; $inlineIndented = true; $newline = true; @@ -196,67 +233,72 @@ public function format(string $string, string $indentString = ' '): string $return = rtrim($return, ' '); } - if (! $inlineParentheses) { + if (! $inlineBlock) { $increaseBlockIndent = true; - // Add a newline after the parentheses - $newline = true; + // Add a newline after the block + if ($newBlockEndCondition->addNewline) { + $newline = true; + } + } + + if ($increaseBlockIndent) { + $indentTypes[] = self::INDENT_BLOCK; } - } elseif ($token->value() === ')') { - // Closing parentheses decrease the block indent level - // Remove whitespace before the closing parentheses - $return = rtrim($return, ' '); + $expectedBlockEnds[] = $newBlockEndCondition; + } + + if ($blockEndCondition && $token->isBlockEnd($blockEndCondition)) { + // Closing block decrease the block indent level + array_pop($expectedBlockEnds); $indentLevel--; // Reset indent level - while ($j = array_shift($indentTypes)) { - if ($j !== 'special') { + while ($lastIndentType = array_pop($indentTypes)) { + if ($lastIndentType !== self::INDENT_SPECIAL) { break; } $indentLevel--; } - if ($indentLevel < 0) { - // This is an error - $indentLevel = 0; - - $return .= $this->highlighter->highlightError($token->value()); - continue; + // Add a newline before the closing block (if not already added) + if (! $addedNewline && $blockEndCondition->addNewline) { + $return .= "\n" . str_repeat($tab, $indentLevel); } + } - // Add a newline before the closing parentheses (if not already added) - if (! $addedNewline) { - $return .= "\n" . str_repeat($tab, $indentLevel); + if ($token->isOfType(Token::TOKEN_TYPE_RESERVED_TOPLEVEL)) { + // VALUES() is also a function + if ($token->value() === 'VALUES') { + $prevNotWhitespace = $cursor->subCursor()->previous(Token::TOKEN_TYPE_WHITESPACE); + if ($prevNotWhitespace && $prevNotWhitespace->value() === '=') { + $return .= ' ' . $highlighted; + continue; + } } - } elseif ($token->isOfType(Token::TOKEN_TYPE_RESERVED_TOPLEVEL)) { + // Top level reserved words start a new line and increase the special indent level $increaseSpecialIndent = true; // If the last indent type was 'special', decrease the special indent for this round - reset($indentTypes); - if (current($indentTypes) === 'special') { + if (end($indentTypes) === self::INDENT_SPECIAL) { $indentLevel--; - array_shift($indentTypes); + array_pop($indentTypes); } // Add a newline after the top level reserved word $newline = true; // Add a newline before the top level reserved word (if not already added) if (! $addedNewline) { - $return = rtrim($return, ' '); $return .= "\n" . str_repeat($tab, $indentLevel); } else { // If we already added a newline, redo the indentation since it may be different now $return = rtrim($return, $tab) . str_repeat($tab, $indentLevel); } - if ($token->hasExtraWhitespace()) { - $highlighted = preg_replace('/\s+/', ' ', $highlighted); - } - - //if SQL 'LIMIT' clause, start variable to reset newline - if ($token->value() === 'LIMIT' && ! $inlineParentheses) { + // if SQL 'LIMIT' clause, start variable to reset newline + if ($token->value() === 'LIMIT' && ! $inlineBlock) { $clauseLimit = true; } } elseif ( @@ -266,9 +308,9 @@ public function format(string $string, string $indentString = ' '): string ) { // Checks if we are out of the limit clause $clauseLimit = false; - } elseif ($token->value() === ',' && ! $inlineParentheses) { - // Commas start a new line (unless within inline parentheses or SQL 'LIMIT' clause) - //If the previous TOKEN_VALUE is 'LIMIT', resets new line + } elseif ($token->value() === ',' && ! $inlineBlock) { + // Commas start a new line (unless within inline block or SQL 'LIMIT' clause) + // If the previous TOKEN_VALUE is 'LIMIT', resets new line if ($clauseLimit === true) { $newline = false; $clauseLimit = false; @@ -277,77 +319,71 @@ public function format(string $string, string $indentString = ' '): string $newline = true; } } elseif ($token->isOfType(Token::TOKEN_TYPE_RESERVED_NEWLINE)) { - // Newline reserved words start a new line + // Newline reserved words start a new line // Add a newline before the reserved word (if not already added) if (! $addedNewline) { - $return = rtrim($return, ' '); $return .= "\n" . str_repeat($tab, $indentLevel); } - - if ($token->hasExtraWhitespace()) { - $highlighted = preg_replace('/\s+/', ' ', $highlighted); - } - } elseif ($token->isOfType(Token::TOKEN_TYPE_BOUNDARY)) { - // Multiple boundary characters in a row should not have spaces between them (not including parentheses) - $prevNotWhitespaceToken = $cursor->subCursor()->previous(Token::TOKEN_TYPE_WHITESPACE); - if ($prevNotWhitespaceToken && $prevNotWhitespaceToken->isOfType(Token::TOKEN_TYPE_BOUNDARY)) { - $prevToken = $cursor->subCursor()->previous(); - if ($prevToken && ! $prevToken->isOfType(Token::TOKEN_TYPE_WHITESPACE)) { - $return = rtrim($return, ' '); - } - } - } - - // If the token shouldn't have a space before it - if ( - $token->value() === '.' || - $token->value() === ',' || - $token->value() === ';' - ) { - $return = rtrim($return, ' '); } - $return .= $highlighted . ' '; + $return .= $highlighted; // If the token shouldn't have a space after it - if ($token->value() === '(' || $token->value() === '.') { - $return = rtrim($return, ' '); - } - - // If this is the "-" of a negative number, it shouldn't have a space after it - if ($token->value() !== '-') { + if ($newline || $token->value() === '(' || $token->value() === '.') { continue; } $nextNotWhitespace = $cursor->subCursor()->next(Token::TOKEN_TYPE_WHITESPACE); - if (! $nextNotWhitespace || ! $nextNotWhitespace->isOfType(Token::TOKEN_TYPE_NUMBER)) { + if (! $nextNotWhitespace) { continue; } - $prev = $cursor->subCursor()->previous(Token::TOKEN_TYPE_WHITESPACE); - if (! $prev) { + if (! $nextNotWhitespace->wantsSpaceBefore()) { continue; } + // If this is the "-" of a negative number, it shouldn't have a space after it + if ($token->value() === '-') { + $prevNotWhitespace = $cursor->subCursor()->previous(Token::TOKEN_TYPE_WHITESPACE); + if (! $prevNotWhitespace) { + continue; + } + + if ( + $nextNotWhitespace->isOfType(Token::TOKEN_TYPE_NUMBER) + && ! $prevNotWhitespace->isOfType( + Token::TOKEN_TYPE_QUOTE, + Token::TOKEN_TYPE_BACKTICK_QUOTE, + Token::TOKEN_TYPE_WORD, + Token::TOKEN_TYPE_NUMBER + ) + ) { + continue; + } + } + + // Don't add whitespace between operators like != <> >= := && etc. if ( - $prev->isOfType( - Token::TOKEN_TYPE_QUOTE, - Token::TOKEN_TYPE_BACKTICK_QUOTE, - Token::TOKEN_TYPE_WORD, - Token::TOKEN_TYPE_NUMBER - ) + $token->isOfType(Token::TOKEN_TYPE_BOUNDARY) + && $token->value() !== ')' + && $nextNotWhitespace->isOfType(Token::TOKEN_TYPE_BOUNDARY) + && ! in_array($nextNotWhitespace->value(), ['(', '-'], true) ) { continue; } - $return = rtrim($return, ' '); + $return .= ' '; + } + + $blockEndCondition = end($expectedBlockEnds); + if ($blockEndCondition && $blockEndCondition->eof) { + array_pop($expectedBlockEnds); } - // If there are unmatched parentheses - if (array_search('block', $indentTypes) !== false) { - $return = rtrim($return, ' '); + // If there are unmatched blocks + if (count($expectedBlockEnds)) { $return .= $this->highlighter->highlightErrorMessage( - 'WARNING: unclosed parentheses or section' + 'WARNING: unclosed block' ); } @@ -399,20 +435,6 @@ public function compress(string $string): string continue; } - // Remove extra whitespace in reserved words (e.g "OUTER JOIN" becomes "OUTER JOIN") - - if ( - $token->isOfType( - Token::TOKEN_TYPE_RESERVED, - Token::TOKEN_TYPE_RESERVED_NEWLINE, - Token::TOKEN_TYPE_RESERVED_TOPLEVEL - ) - ) { - $newValue = preg_replace('/\s+/', ' ', $token->value()); - assert($newValue !== null); - $token = $token->withValue($newValue); - } - if ($token->isOfType(Token::TOKEN_TYPE_WHITESPACE)) { // If the last token was whitespace, don't add another one if ($whitespace) { diff --git a/src/Token.php b/src/Token.php index b3be48b..bb27a19 100644 --- a/src/Token.php +++ b/src/Token.php @@ -5,7 +5,6 @@ namespace Doctrine\SqlFormatter; use function in_array; -use function strpos; final class Token { @@ -55,15 +54,75 @@ public function isOfType(int ...$types): bool return in_array($this->type, $types, true); } - public function hasExtraWhitespace(): bool + public function withValue(string $value): self { - return strpos($this->value(), ' ') !== false || - strpos($this->value(), "\n") !== false || - strpos($this->value(), "\t") !== false; + return new self($this->type(), $value); } - public function withValue(string $value): self + public function isBlockStart(): ?Condition { - return new self($this->type(), $value); + $condition = new Condition(); + + if ($this->value === '(') { + $condition->values = [')']; + $condition->addNewline = true; + + return $condition; + } + + if ($this->value === 'CASE WHEN' || $this->value === 'ELSE') { + $condition->values = ['ELSE', 'END']; + + return $condition; + } + + if ($this->value === 'CASE') { + $condition->values = ['END']; + + return $condition; + } + + $joins = [ + 'LEFT OUTER JOIN', + 'RIGHT OUTER JOIN', + 'LEFT JOIN', + 'RIGHT JOIN', + 'OUTER JOIN', + 'INNER JOIN', + 'CROSS JOIN', + 'JOIN', + ]; + if (in_array($this->value, $joins, true)) { + $condition->values = $joins; + $condition->types = [self::TOKEN_TYPE_RESERVED_TOPLEVEL]; + $condition->eof = true; + + return $condition; + } + + return null; + } + + public function isBlockEnd(Condition $condition): bool + { + if ($this->isOfType(...$condition->types)) { + return true; + } + + return in_array($this->value, $condition->values, true); + } + + public function wantsSpaceBefore(): bool + { + if (in_array($this->value, ['.', ',', ';', ')'], true)) { + return false; + } + + return ! $this->isOfType( + self::TOKEN_TYPE_RESERVED_NEWLINE, + self::TOKEN_TYPE_RESERVED_TOPLEVEL, + self::TOKEN_TYPE_COMMENT, + self::TOKEN_TYPE_BLOCK_COMMENT + ); } } diff --git a/src/Tokenizer.php b/src/Tokenizer.php index ac9fb7a..e0cd526 100644 --- a/src/Tokenizer.php +++ b/src/Tokenizer.php @@ -12,6 +12,7 @@ use function implode; use function preg_match; use function preg_quote; +use function preg_replace; use function str_replace; use function strlen; use function strpos; @@ -49,7 +50,6 @@ final class Tokenizer 'BINLOG', 'BOTH', 'CASCADE', - 'CASE', 'CHANGE', 'CHANGED', 'CHARACTER SET', @@ -92,9 +92,7 @@ final class Tokenizer 'DUMPFILE', 'DUPLICATE', 'DYNAMIC', - 'ELSE', 'ENCLOSED', - 'END', 'ENGINE', 'ENGINE_TYPE', 'ENGINES', @@ -189,7 +187,6 @@ final class Tokenizer 'NOW()', 'NULL', 'OFFSET', - 'ON', 'OPEN', 'OPTIMIZE', 'OPTION', @@ -284,7 +281,6 @@ final class Tokenizer 'TABLES', 'TEMPORARY', 'TERMINATED', - 'THEN', 'TIES', 'TO', 'TRAILING', @@ -303,7 +299,6 @@ final class Tokenizer 'USING', 'VARIABLES', 'VIEW', - 'WHEN', 'WITH', 'WORK', 'WRITE', @@ -343,6 +338,7 @@ final class Tokenizer 'RANGE', 'GROUPS', 'WINDOW', + 'ON DUPLICATE KEY UPDATE', ]; /** @var string[] */ @@ -353,11 +349,18 @@ final class Tokenizer 'RIGHT JOIN', 'OUTER JOIN', 'INNER JOIN', + 'CROSS JOIN', 'JOIN', 'XOR', 'OR', 'AND', 'EXCLUDE', + 'ON', + 'CASE WHEN', + 'CASE', + 'WHEN', + 'ELSE', + 'END', ]; /** @var string[] */ @@ -669,6 +672,7 @@ final class Tokenizer 'UTC_TIME', 'UTC_TIMESTAMP', 'UUID', + 'VALUES', 'VAR', 'VARIANCE', 'VARP', @@ -909,7 +913,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED_TOPLEVEL, - substr($upper, 0, strlen($matches[1])) + (string) preg_replace('/\s+/', ' ', substr($upper, 0, strlen($matches[1]))) ); } @@ -923,7 +927,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED_NEWLINE, - substr($upper, 0, strlen($matches[1])) + (string) preg_replace('/\s+/', ' ', substr($upper, 0, strlen($matches[1]))) ); } @@ -937,7 +941,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED, - substr($upper, 0, strlen($matches[1])) + (string) preg_replace('/\s+/', ' ', substr($upper, 0, strlen($matches[1]))) ); } } diff --git a/tests/clihighlight.html b/tests/clihighlight.html index d81ca08..96184a5 100755 --- a/tests/clihighlight.html +++ b/tests/clihighlight.html @@ -4,7 +4,14 @@ [37mCOUNT[0m(order_id[0m) [37mAS[0m total[0m [37mFROM[0m customers[0m - [37mINNER JOIN[0m orders[0m [37mON[0m customers[0m.[0mcustomer_id[0m =[0m orders[0m.[0mcustomer_id[0m + [37mINNER JOIN[0m orders[0m + [37mON[0m customers[0m.[0mcustomer_id[0m =[0m orders[0m.[0mcustomer_id[0m + [37mAND[0m orders[0m.[0mtype[0m =[0m [32;1m1[0m + [37mOR[0m orders[0m.[0mtype[0m =[0m [32;1m2[0m + [37mINNER JOIN[0m orders2[0m + [37mON[0m customers[0m.[0mcustomer_id2[0m =[0m orders2[0m.[0mcustomer_id[0m + [37mAND[0m orders2[0m.[0mtype[0m =[0m [32;1m1[0m + [37mOR[0m orders2[0m.[0mtype[0m =[0m [32;1m2[0m [37mGROUP BY[0m customer_id[0m,[0m customer_name[0m @@ -509,7 +516,8 @@ [34;1m'0000-00-00 00:00:00'[0m [37mFROM[0m [35;1m`PREFIX_discount_quantity`[0m dq[0m - [37mINNER JOIN[0m [35;1m`PREFIX_product`[0m p[0m [37mON[0m (p[0m.[0m[35;1m`id_product`[0m =[0m dq[0m.[0m[35;1m`id_product`[0m) + [37mINNER JOIN[0m [35;1m`PREFIX_product`[0m p[0m + [37mON[0m (p[0m.[0m[35;1m`id_product`[0m =[0m dq[0m.[0m[35;1m`id_product`[0m) ) --- [37mDROP[0m @@ -744,12 +752,11 @@ [37mFROM[0m Test[0m [37mWHERE[0m - (MyColumn[0m =[0m [32;1m1[0m) -[31;1;7m)[0m -[37mAND[0m ( - ( - (SomeOtherColumn[0m =[0m [32;1m2[0m);[0m -[31;1;7mWARNING: unclosed parentheses or section[0m + (MyColumn[0m =[0m [32;1m1[0m)) + [37mAND[0m ( + ( + (SomeOtherColumn[0m =[0m [32;1m2[0m);[0m +[31;1;7mWARNING: unclosed block[0m --- [37mALTER TABLE[0m [35;1m`test_modify`[0m @@ -823,7 +830,8 @@ a[0m [37mFROM[0m b[0m - [37mLEFT OUTER JOIN[0m c[0m [37mON[0m (d[0m =[0m f[0m);[0m + [37mLEFT OUTER JOIN[0m c[0m + [37mON[0m (d[0m =[0m f[0m);[0m --- [37mWITH[0m cte[0m [37mAS[0m ( @@ -976,5 +984,123 @@ hire_date[0m );[0m --- +[37mSELECT[0m + [37mSQL_NO_CACHE[0m r[0m.[0mname[0m,[0m + r[0m.[0mtime[0m,[0m + [37mCASE WHEN[0m r[0m.[0mmessage[0m [37mLIKE[0m [34;1m'old_service %'[0m THEN[0m + [30;1m-- Extract old service name from service message[0m + [37mSUBSTRING_INDEX[0m( + [37mSUBSTRING_INDEX[0m( + [37mSUBSTRING_INDEX[0m(r[0m.[0mmessage[0m,[0m [34;1m'from '[0m,[0m -[0m[32;1m1[0m),[0m + [34;1m' ('[0m,[0m + [32;1m1[0m + ),[0m + [34;1m' to '[0m,[0m + [32;1m1[0m + ) + [37mELSE[0m + [30;1m-- Extract old service name from other message[0m + [37mSUBSTRING_INDEX[0m( + [37mSUBSTRING_INDEX[0m(r[0m.[0mmessage[0m,[0m [34;1m'service '[0m,[0m -[0m[32;1m1[0m),[0m + [34;1m' ('[0m,[0m + [32;1m1[0m + ) + [37mEND[0m old_service[0m,[0m + [37mCASE WHEN[0m r[0m.[0mmessage[0m [37mLIKE[0m [34;1m'service %'[0m THEN[0m + [30;1m-- Extract service name from service message[0m + [37mSUBSTRING_INDEX[0m( + [37mSUBSTRING_INDEX[0m( + [37mSUBSTRING_INDEX[0m(r[0m.[0mmessage[0m,[0m [34;1m'from '[0m,[0m -[0m[32;1m1[0m),[0m + [34;1m' ('[0m,[0m + [32;1m1[0m + ),[0m + [34;1m' to '[0m,[0m + -[0m[32;1m1[0m + ) + [37mELSE[0m + [30;1m-- Extract service name from other message[0m + [34;1m'Default'[0m + [37mEND[0m new_service[0m,[0m + p[0m.[0mname[0m current_service[0m,[0m + [37mCASE[0m r[0m.[0mname[0m + [37mWHEN[0m [34;1m'pre-fix'[0m THEN[0m [34;1m'pre'[0m + [37mWHEN[0m [34;1m'post-fix'[0m THEN[0m [34;1m'post'[0m + [37mEND[0m [37mAS[0m fix[0m +[37mFROM[0m + ( + [37mSELECT[0m + entries[0m.[0m*[0m,[0m + [30;1m-- Variable to count rows by name[0m + ( + [36;1m@rn[0m :[0m=[0m [37mIF[0m( + [36;1m@id[0m =[0m entries[0m.[0mname[0m,[0m + [36;1m@rn[0m +[0m [32;1m1[0m,[0m + [37mIF[0m([36;1m@id[0m :[0m=[0m entries[0m.[0mname[0m,[0m [32;1m1[0m,[0m [32;1m1[0m) + ) + ) entry_order[0m + [37mFROM[0m + ( + [30;1m-- Name is second word in both cases, ordering and cancelling a service[0m + [37mSELECT[0m + [37mSUBSTRING_INDEX[0m( + [37mSUBSTRING_INDEX[0m(message[0m,[0m [34;1m' '[0m,[0m [32;1m2[0m),[0m + [34;1m' '[0m,[0m + -[0m[32;1m1[0m + ) name[0m,[0m + time[0m,[0m + message[0m + [37mFROM[0m + log[0m + [30;1m-- Only logged messages about service ordering or cancelling[0m + [37mWHERE[0m + ( + message[0m [37mLIKE[0m [34;1m'cancelled % service %'[0m + [37mOR[0m message[0m [37mLIKE[0m [34;1m'ordered % service %'[0m + ) + [37mAND[0m time[0m >[0m=[0m ?[0m + [37mAND[0m time[0m <[0m ?[0m + [37mORDER BY[0m + name[0m,[0m + time[0m [37mDESC[0m + ) entries[0m + [37mCROSS JOIN[0m ( + [37mSELECT[0m + [36;1m@rn[0m :[0m=[0m [32;1m0[0m,[0m + [36;1m@id[0m :[0m=[0m -[0m[32;1m1[0m + ) params[0m + ) r[0m + [37mINNER JOIN[0m serivce[0m s[0m + [37mON[0m s[0m.[0mname[0m =[0m r[0m.[0mname[0m + [37mINNER JOIN[0m [35;1m`order`[0m o[0m + [37mON[0m o[0m.[0mid[0m =[0m d[0m.[0morder_id[0m + [37mINNER JOIN[0m product[0m p[0m + [37mON[0m p[0m.[0mid[0m =[0m o[0m.[0mproduct_id[0m +[37mWHERE[0m + [30;1m-- Only keep the latest entry[0m + r[0m.[0mentry_order[0m =[0m [32;1m1[0m + [37mAND[0m o[0m.[0mis_active[0m =[0m [32;1m1[0m + [30;1m-- No external products[0m + [37mAND[0m p[0m.[0mexternal[0m =[0m [32;1m0[0m + [37mAND[0m ( + p[0m.[0mtype[0m =[0m [32;1m1[0m + [37mOR[0m p[0m.[0mid[0m =[0m [32;1m7[0m + ) + [37mAND[0m [37mMD5[0m( + [37mCONCAT[0m(p[0m.[0mtype[0m,[0m ?[0m) + ) =[0m [34;1m'11'[0m +[37mHAVING[0m + new_service[0m =[0m [34;1m'Old'[0m + [30;1m-- Skip Old -> Old service[0m + [37mAND[0m old_service[0m <[0m>[0m [34;1m'Old'[0m +[37mORDER BY[0m + old_service[0m,[0m + r[0m.[0mname[0m [37mASC[0m +--- +[37mINSERT[0m [37mINTO[0m customers[0m (id[0m,[0m username[0m) +[37mVALUES[0m + ([32;1m1[0m,[0m [34;1m'Joe'[0m) +[37mON DUPLICATE KEY UPDATE[0m + username[0m =[0m [37mVALUES[0m(username[0m) +--- [37mSELECT[0m [32;1m1[0m ::[0m text[0m;[0m diff --git a/tests/compress.html b/tests/compress.html index 214fc50..03fa6c9 100755 --- a/tests/compress.html +++ b/tests/compress.html @@ -1,4 +1,4 @@ -SELECT customer_id, customer_name, COUNT(order_id) AS total FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customer_id, customer_name HAVING COUNT(order_id) > 5 ORDER BY COUNT(order_id) DESC; +SELECT customer_id, customer_name, COUNT(order_id) AS total FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id AND orders.type = 1 OR orders.type = 2 INNER JOIN orders2 ON customers.customer_id2 = orders2.customer_id AND orders2.type = 1 OR orders2.type = 2 GROUP BY customer_id, customer_name HAVING COUNT(order_id) > 5 ORDER BY COUNT(order_id) DESC; --- UPDATE customers SET totalorders = ordersummary.total FROM (SELECT customer_id, COUNT(order_id) AS total FROM orders GROUP BY customer_id) AS ordersummary WHERE customers.customer_id = ordersummary.customer_id --- @@ -93,4 +93,8 @@ --- SELECT a, GROUP_CONCAT(b, '.') OVER (ORDER BY c GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS no_others, GROUP_CONCAT(b, '.') OVER (ORDER BY c GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW) AS current_row, GROUP_CONCAT(b, '.') OVER (ORDER BY c GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE GROUP) AS grp, GROUP_CONCAT(b, '.') OVER (ORDER BY c GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE TIES) AS tie, GROUP_CONCAT(b, '.') FILTER (WHERE c != 'two') OVER (ORDER BY a) AS filtered, CONVERT(VARCHAR(20), AVG(SalesYTD) OVER (PARTITION BY TerritoryID ORDER BY DATEPART(yy, ModifiedDate)), 1) AS MovingAvg, AVG(starting_salary) OVER w2 AVG, MIN(starting_salary) OVER w2 MIN_STARTING_SALARY, MAX(starting_salary) OVER (w1 ORDER BY hire_date), LISTAGG(arg, ',') OVER (PARTITION BY part ORDER BY ord ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS LISTAGG_ROWS, LISTAGG(arg, ',') OVER (PARTITION BY part ORDER BY ord RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS LISTAGG_RANGE, MIN(Revenue) OVER (PARTITION BY DepartmentID ORDER BY RevenueYear ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS MinRevenueBeyond FROM t1 WINDOW w1 AS (PARTITION BY department, division), w2 AS (w1 ORDER BY hire_date); --- +SELECT SQL_NO_CACHE r.name, r.time, CASE WHEN r.message LIKE 'old_service %' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'from ',-1),' (',1),' to ',1) ELSE SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'service ',-1),' (',1) END old_service, CASE WHEN r.message LIKE 'service %' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'from ',-1),' (',1),' to ',-1) ELSE 'Default' END new_service, p.name current_service, CASE r.name WHEN 'pre-fix' THEN 'pre' WHEN 'post-fix' THEN 'post' END AS fix FROM ( SELECT entries.*, ( @rn := IF( @id = entries.name, @rn + 1, IF(@id := entries.name, 1, 1) ) ) entry_order FROM ( SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(message,' ',2),' ',-1) name, time, message FROM log WHERE ( message LIKE 'cancelled % service %' OR message LIKE 'ordered % service %' ) AND time >= ? AND time < ? ORDER BY name, time DESC ) entries CROSS JOIN ( SELECT @rn := 0, @id := -1 ) params ) r INNER JOIN serivce s ON s.name = r.name INNER JOIN `order` o ON o.id = d.order_id INNER JOIN product p ON p.id = o.product_id WHERE r.entry_order = 1 AND o.is_active = 1 AND p.external = 0 AND (p.type = 1 OR p.id = 7) AND MD5(CONCAT(p.type, ?)) = '11' HAVING new_service = 'Old' AND old_service <> 'Old' ORDER BY old_service, r.name ASC +--- +INSERT INTO customers (id, username) VALUES (1, 'Joe') ON DUPLICATE KEY UPDATE username = VALUES(username) +--- SELECT 1::text; diff --git a/tests/format-highlight.html b/tests/format-highlight.html index a0c660e..f80a4f6 100755 --- a/tests/format-highlight.html +++ b/tests/format-highlight.html @@ -4,7 +4,14 @@ COUNT(order_id) AS total FROM customers - INNER JOIN orders ON customers.customer_id = orders.customer_id + INNER JOIN orders + ON customers.customer_id = orders.customer_id + AND orders.type = 1 + OR orders.type = 2 + INNER JOIN orders2 + ON customers.customer_id2 = orders2.customer_id + AND orders2.type = 1 + OR orders2.type = 2 GROUP BY customer_id, customer_name @@ -509,7 +516,8 @@ '0000-00-00 00:00:00' FROM `PREFIX_discount_quantity` dq - INNER JOIN `PREFIX_product` p ON (p.`id_product` = dq.`id_product`) + INNER JOIN `PREFIX_product` p + ON (p.`id_product` = dq.`id_product`) ) ---
DROP @@ -744,12 +752,11 @@ FROM Test WHERE - (MyColumn = 1) -) -AND ( - ( - (SomeOtherColumn = 2); -WARNING: unclosed parentheses or section+ (MyColumn = 1)) + AND ( + ( + (SomeOtherColumn = 2); +WARNING: unclosed block ---
ALTER TABLE `test_modify` @@ -823,7 +830,8 @@ a FROM b - LEFT OUTER JOIN c ON (d = f);+ LEFT OUTER JOIN c + ON (d = f); ---
WITH cte AS ( @@ -976,5 +984,123 @@ hire_date );--- +
SELECT + SQL_NO_CACHE r.name, + r.time, + CASE WHEN r.message LIKE 'old_service %' THEN + -- Extract old service name from service message + SUBSTRING_INDEX( + SUBSTRING_INDEX( + SUBSTRING_INDEX(r.message, 'from ', -1), + ' (', + 1 + ), + ' to ', + 1 + ) + ELSE + -- Extract old service name from other message + SUBSTRING_INDEX( + SUBSTRING_INDEX(r.message, 'service ', -1), + ' (', + 1 + ) + END old_service, + CASE WHEN r.message LIKE 'service %' THEN + -- Extract service name from service message + SUBSTRING_INDEX( + SUBSTRING_INDEX( + SUBSTRING_INDEX(r.message, 'from ', -1), + ' (', + 1 + ), + ' to ', + -1 + ) + ELSE + -- Extract service name from other message + 'Default' + END new_service, + p.name current_service, + CASE r.name + WHEN 'pre-fix' THEN 'pre' + WHEN 'post-fix' THEN 'post' + END AS fix +FROM + ( + SELECT + entries.*, + -- Variable to count rows by name + ( + @rn := IF( + @id = entries.name, + @rn + 1, + IF(@id := entries.name, 1, 1) + ) + ) entry_order + FROM + ( + -- Name is second word in both cases, ordering and cancelling a service + SELECT + SUBSTRING_INDEX( + SUBSTRING_INDEX(message, ' ', 2), + ' ', + -1 + ) name, + time, + message + FROM + log + -- Only logged messages about service ordering or cancelling + WHERE + ( + message LIKE 'cancelled % service %' + OR message LIKE 'ordered % service %' + ) + AND time >= ? + AND time < ? + ORDER BY + name, + time DESC + ) entries + CROSS JOIN ( + SELECT + @rn := 0, + @id := -1 + ) params + ) r + INNER JOIN serivce s + ON s.name = r.name + INNER JOIN `order` o + ON o.id = d.order_id + INNER JOIN product p + ON p.id = o.product_id +WHERE + -- Only keep the latest entry + r.entry_order = 1 + AND o.is_active = 1 + -- No external products + AND p.external = 0 + AND ( + p.type = 1 + OR p.id = 7 + ) + AND MD5( + CONCAT(p.type, ?) + ) = '11' +HAVING + new_service = 'Old' + -- Skip Old -> Old service + AND old_service <> 'Old' +ORDER BY + old_service, + r.name ASC+--- +
INSERT INTO customers (id, username) +VALUES + (1, 'Joe') +ON DUPLICATE KEY UPDATE + username = VALUES(username)+---
SELECT 1 :: text;diff --git a/tests/format.html b/tests/format.html index 8f5a671..ea5622e 100755 --- a/tests/format.html +++ b/tests/format.html @@ -4,7 +4,14 @@ COUNT(order_id) AS total FROM customers - INNER JOIN orders ON customers.customer_id = orders.customer_id + INNER JOIN orders + ON customers.customer_id = orders.customer_id + AND orders.type = 1 + OR orders.type = 2 + INNER JOIN orders2 + ON customers.customer_id2 = orders2.customer_id + AND orders2.type = 1 + OR orders2.type = 2 GROUP BY customer_id, customer_name @@ -509,7 +516,8 @@ '0000-00-00 00:00:00' FROM `PREFIX_discount_quantity` dq - INNER JOIN `PREFIX_product` p ON (p.`id_product` = dq.`id_product`) + INNER JOIN `PREFIX_product` p + ON (p.`id_product` = dq.`id_product`) ) --- DROP @@ -745,9 +753,9 @@ Test WHERE (MyColumn = 1)) -AND ( - ( - (SomeOtherColumn = 2); WARNING: unclosed parentheses or section + AND ( + ( + (SomeOtherColumn = 2); WARNING: unclosed block --- ALTER TABLE `test_modify` @@ -821,7 +829,8 @@ a FROM b - LEFT OUTER JOIN c ON (d = f); + LEFT OUTER JOIN c + ON (d = f); --- WITH cte AS ( @@ -974,5 +983,123 @@ hire_date ); --- +SELECT + SQL_NO_CACHE r.name, + r.time, + CASE WHEN r.message LIKE 'old_service %' THEN + -- Extract old service name from service message + SUBSTRING_INDEX( + SUBSTRING_INDEX( + SUBSTRING_INDEX(r.message, 'from ', -1), + ' (', + 1 + ), + ' to ', + 1 + ) + ELSE + -- Extract old service name from other message + SUBSTRING_INDEX( + SUBSTRING_INDEX(r.message, 'service ', -1), + ' (', + 1 + ) + END old_service, + CASE WHEN r.message LIKE 'service %' THEN + -- Extract service name from service message + SUBSTRING_INDEX( + SUBSTRING_INDEX( + SUBSTRING_INDEX(r.message, 'from ', -1), + ' (', + 1 + ), + ' to ', + -1 + ) + ELSE + -- Extract service name from other message + 'Default' + END new_service, + p.name current_service, + CASE r.name + WHEN 'pre-fix' THEN 'pre' + WHEN 'post-fix' THEN 'post' + END AS fix +FROM + ( + SELECT + entries.*, + -- Variable to count rows by name + ( + @rn := IF( + @id = entries.name, + @rn + 1, + IF(@id := entries.name, 1, 1) + ) + ) entry_order + FROM + ( + -- Name is second word in both cases, ordering and cancelling a service + SELECT + SUBSTRING_INDEX( + SUBSTRING_INDEX(message, ' ', 2), + ' ', + -1 + ) name, + time, + message + FROM + log + -- Only logged messages about service ordering or cancelling + WHERE + ( + message LIKE 'cancelled % service %' + OR message LIKE 'ordered % service %' + ) + AND time >= ? + AND time < ? + ORDER BY + name, + time DESC + ) entries + CROSS JOIN ( + SELECT + @rn := 0, + @id := -1 + ) params + ) r + INNER JOIN serivce s + ON s.name = r.name + INNER JOIN `order` o + ON o.id = d.order_id + INNER JOIN product p + ON p.id = o.product_id +WHERE + -- Only keep the latest entry + r.entry_order = 1 + AND o.is_active = 1 + -- No external products + AND p.external = 0 + AND ( + p.type = 1 + OR p.id = 7 + ) + AND MD5( + CONCAT(p.type, ?) + ) = '11' +HAVING + new_service = 'Old' + -- Skip Old -> Old service + AND old_service <> 'Old' +ORDER BY + old_service, + r.name ASC +--- +INSERT INTO customers (id, username) +VALUES + (1, 'Joe') +ON DUPLICATE KEY UPDATE + username = VALUES(username) +--- SELECT 1 :: text; diff --git a/tests/highlight.html b/tests/highlight.html index 7fc5c9b..970c1b2 100755 --- a/tests/highlight.html +++ b/tests/highlight.html @@ -1,5 +1,6 @@
SELECT customer_id, customer_name, COUNT(order_id) AS total -FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id +FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id AND orders.type = 1 OR orders.type = 2 +INNER JOIN orders2 ON customers.customer_id2 = orders2.customer_id AND orders2.type = 1 OR orders2.type = 2 GROUP BY customer_id, customer_name HAVING COUNT(order_id) > 5 ORDER BY COUNT(order_id) DESC;@@ -269,9 +270,7 @@ ---
SELECT [sqlserver] FROM [escap[e]]d style];--- -
SELECT a FROM b LEFT -OUTER -JOIN c ON (d=f);+
SELECT a FROM b LEFT OUTER JOIN c ON (d=f);---
WITH cte AS (SELECT a, b FROM `table`), RECURSIVE fibonacci (n, fib_n, next_fib_n) AS ( @@ -303,4 +302,86 @@ FROM t1 WINDOW w1 AS (PARTITION BY department, division), w2 AS (w1 ORDER BY hire_date);--- +
SELECT + SQL_NO_CACHE r.name, + r.time, + CASE WHEN r.message LIKE 'old_service %' THEN + -- Extract old service name from service message + SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'from ',-1),' (',1),' to ',1) + ELSE + -- Extract old service name from other message + SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'service ',-1),' (',1) + END old_service, + CASE WHEN r.message LIKE 'service %' THEN + -- Extract service name from service message + SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'from ',-1),' (',1),' to ',-1) + ELSE + -- Extract service name from other message + 'Default' + END new_service, + p.name current_service, + CASE r.name WHEN 'pre-fix' THEN 'pre' WHEN 'post-fix' THEN 'post' END AS fix +FROM + ( + SELECT + entries.*, + -- Variable to count rows by name + ( + @rn := IF( + @id = entries.name, + @rn + 1, + IF(@id := entries.name, 1, 1) + ) + ) entry_order + FROM + ( + -- Name is second word in both cases, ordering and cancelling a service + SELECT + SUBSTRING_INDEX(SUBSTRING_INDEX(message,' ',2),' ',-1) name, + time, + message + FROM + log + -- Only logged messages about service ordering or cancelling + WHERE + ( + message LIKE 'cancelled % service %' + OR message LIKE 'ordered % service %' + ) + AND time >= ? + AND time < ? + ORDER BY + name, + time DESC + ) entries + CROSS JOIN ( + SELECT + @rn := 0, + @id := -1 + ) params + ) r +INNER JOIN serivce s ON s.name = r.name +INNER JOIN `order` o ON o.id = d.order_id +INNER JOIN product p ON p.id = o.product_id +WHERE + -- Only keep the latest entry + r.entry_order = 1 + AND o.is_active = 1 + -- No external products + AND p.external = 0 + AND (p.type = 1 OR p.id = 7) + AND MD5(CONCAT(p.type, ?)) = '11' +HAVING + new_service = 'Old' + -- Skip Old -> Old service + AND old_service <> 'Old' +ORDER BY + old_service, + r.name ASC+--- +
INSERT INTO customers (id, username) +VALUES (1, 'Joe') +ON DUPLICATE KEY UPDATE +username = VALUES(username)+---
SELECT 1::text;diff --git a/tests/sql.sql b/tests/sql.sql index bda9509..518f213 100755 --- a/tests/sql.sql +++ b/tests/sql.sql @@ -1,5 +1,6 @@ SELECT customer_id, customer_name, COUNT(order_id) as total -FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id +FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id AND orders.type = 1 OR orders.type = 2 +INNER JOIN orders2 ON customers.customer_id2 = orders2.customer_id AND orders2.type = 1 OR orders2.type = 2 GROUP BY customer_id, customer_name HAVING COUNT(order_id) > 5 ORDER BY COUNT(order_id) DESC; @@ -303,4 +304,86 @@ SELECT a, FROM t1 WINDOW w1 AS (PARTITION BY department, division), w2 AS (w1 ORDER BY hire_date); --- +SELECT + SQL_NO_CACHE r.name, + r.time, + CASE WHEN r.message LIKE 'old_service %' THEN + -- Extract old service name from service message + SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'from ',-1),' (',1),' to ',1) + ELSE + -- Extract old service name from other message + SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'service ',-1),' (',1) + END old_service, + CASE WHEN r.message LIKE 'service %' THEN + -- Extract service name from service message + SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(r.message,'from ',-1),' (',1),' to ',-1) + ELSE + -- Extract service name from other message + 'Default' + END new_service, + p.name current_service, + CASE r.name WHEN 'pre-fix' THEN 'pre' WHEN 'post-fix' THEN 'post' END AS fix +FROM + ( + SELECT + entries.*, + -- Variable to count rows by name + ( + @rn := IF( + @id = entries.name, + @rn + 1, + IF(@id := entries.name, 1, 1) + ) + ) entry_order + FROM + ( + -- Name is second word in both cases, ordering and cancelling a service + SELECT + SUBSTRING_INDEX(SUBSTRING_INDEX(message,' ',2),' ',-1) name, + time, + message + FROM + log + -- Only logged messages about service ordering or cancelling + WHERE + ( + message LIKE 'cancelled % service %' + OR message LIKE 'ordered % service %' + ) + AND time >= ? + AND time < ? + ORDER BY + name, + time DESC + ) entries + CROSS JOIN ( + SELECT + @rn := 0, + @id := -1 + ) params + ) r +INNER JOIN serivce s ON s.name = r.name +INNER JOIN `order` o ON o.id = d.order_id +INNER JOIN product p ON p.id = o.product_id +WHERE + -- Only keep the latest entry + r.entry_order = 1 + AND o.is_active = 1 + -- No external products + AND p.external = 0 + AND (p.type = 1 OR p.id = 7) + AND MD5(CONCAT(p.type, ?)) = '11' +HAVING + new_service = 'Old' + -- Skip Old -> Old service + AND old_service <> 'Old' +ORDER BY + old_service, + r.name ASC +--- +INSERT INTO customers (id, username) +VALUES (1, 'Joe') +ON DUPLICATE KEY UPDATE +username = VALUES(username) +--- SELECT 1::text;