From b589eae87c288107358d9e767a61298df954870a Mon Sep 17 00:00:00 2001 From: Rauno Moisto Date: Mon, 14 Aug 2023 17:47:09 +0300 Subject: [PATCH 1/9] Prettier joins --- src/SqlFormatter.php | 143 ++++++++++++++++++++++++++----------------- src/Token.php | 46 ++++++++++++++ src/Tokenizer.php | 4 +- tests/format.html | 21 +++++-- tests/sql.sql | 3 +- 5 files changed, 152 insertions(+), 65 deletions(-) diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php index 624c3bb..ca1c8f2 100644 --- a/src/SqlFormatter.php +++ b/src/SqlFormatter.php @@ -28,6 +28,9 @@ final class SqlFormatter { + private const INDENT_BLOCK = 1; + private const INDENT_SPECIAL = 2; + /** @var Highlighter */ private $highlighter; @@ -57,14 +60,14 @@ public function format(string $string, string $indentString = ' '): string $indentLevel = 0; $newline = false; - $inlineParentheses = false; + $inlineBlock = false; $increaseSpecialIndent = false; $increaseBlockIndent = false; $indentTypes = []; - $addedNewline = false; $inlineCount = 0; $inlineIndented = false; $clauseLimit = false; + $expectedBlockEnds = []; // Tokenize String $cursor = $this->tokenizer->tokenize($string); @@ -80,24 +83,22 @@ public function format(string $string, string $indentString = ' '): string if ($increaseSpecialIndent) { $indentLevel++; $increaseSpecialIndent = false; - array_unshift($indentTypes, 'special'); + $indentTypes[] = self::INDENT_SPECIAL; } // If we are increasing the block indent level now if ($increaseBlockIndent) { $indentLevel++; $increaseBlockIndent = false; - array_unshift($indentTypes, 'block'); } // If we need a new line before the token + $addedNewline = false; if ($newline) { $return = rtrim($return, ' '); $return .= "\n" . str_repeat($tab, $indentLevel); $newline = false; $addedNewline = true; - } else { - $addedNewline = false; } // Display comments directly where they appear in the source @@ -114,19 +115,37 @@ public function format(string $string, string $indentString = ' '): string continue; } - if ($inlineParentheses) { - // End of inline parentheses - if ($token->value() === ')') { + // Allow another block to finish an EOF type block + if (count($expectedBlockEnds) > 1) { + $last = end($expectedBlockEnds); + $prev = prev($expectedBlockEnds); + + if ($token->isBlockEnd($prev) && !in_array(Token::TOKEN_TYPE_EOF, ($prev['types'] ?? []), true)) { + if (isset($last['types']) && in_array(Token::TOKEN_TYPE_EOF, ($last['types'] ?? []), true)) { + // TODO loop instead? + array_pop($expectedBlockEnds); + array_pop($indentTypes); + $indentLevel--; + } + } + } + + $blockEndCondition = end($expectedBlockEnds); + + if ($inlineBlock) { + // End of inline block + if ($blockEndCondition && $token->isBlockEnd($blockEndCondition)) { + array_pop($expectedBlockEnds); $return = rtrim($return, ' '); if ($inlineIndented) { - array_shift($indentTypes); + array_pop($indentTypes); $indentLevel--; $return = rtrim($return, ' '); $return .= "\n" . str_repeat($tab, $indentLevel); } - $inlineParentheses = false; + $inlineBlock = false; $return .= $highlighted . ' '; continue; @@ -142,11 +161,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 !== false) { + // 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 +177,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 +194,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,48 +217,52 @@ 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 + + $expectedBlockEnds[] = $newBlockEndCondition; + } + + if ($blockEndCondition && $token->isBlockEnd($blockEndCondition)) { + // Closing block decrease the block indent level + // Remove whitespace before the closing block $return = rtrim($return, ' '); + 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 parentheses (if not already added) - if (! $addedNewline) { + // Add a newline before the closing block (if not already added) + if (! $addedNewline && $blockEndCondition['addNewline']) { $return .= "\n" . str_repeat($tab, $indentLevel); } - } elseif ($token->isOfType(Token::TOKEN_TYPE_RESERVED_TOPLEVEL)) { + } + + if ($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 @@ -255,8 +280,8 @@ public function format(string $string, string $indentString = ' '): string $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,8 +291,8 @@ 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) + } 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; @@ -277,7 +302,7 @@ 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, ' '); @@ -343,11 +368,16 @@ public function format(string $string, string $indentString = ' '): string $return = rtrim($return, ' '); } - // If there are unmatched parentheses - if (array_search('block', $indentTypes) !== false) { + $blockEndCondition = end($expectedBlockEnds); + if ($blockEndCondition && in_array(Token::TOKEN_TYPE_EOF, $blockEndCondition['types'] ?? [], true)) { + array_pop($expectedBlockEnds); + } + + // If there are unmatched blocks + if (count($expectedBlockEnds)) { $return = rtrim($return, ' '); $return .= $this->highlighter->highlightErrorMessage( - 'WARNING: unclosed parentheses or section' + 'WARNING: unclosed block' ); } @@ -400,6 +430,7 @@ public function compress(string $string): string } // Remove extra whitespace in reserved words (e.g "OUTER JOIN" becomes "OUTER JOIN") + // TODO move to Tokenizer if ( $token->isOfType( diff --git a/src/Token.php b/src/Token.php index b3be48b..557030d 100644 --- a/src/Token.php +++ b/src/Token.php @@ -23,6 +23,7 @@ final class Token public const TOKEN_TYPE_NUMBER = 10; public const TOKEN_TYPE_ERROR = 11; public const TOKEN_TYPE_VARIABLE = 12; + public const TOKEN_TYPE_EOF = 13; // Constants for different components of a token public const TOKEN_TYPE = 0; @@ -66,4 +67,49 @@ public function withValue(string $value): self { return new self($this->type(), $value); } + + public function isBlockStart() + { + if ($this->value === '(') { + return [ + 'values' => [')'], + 'addNewline' => true, + ]; + } + + $joins = [ + 'LEFT OUTER JOIN', + 'RIGHT OUTER JOIN', + 'LEFT JOIN', + 'RIGHT JOIN', + 'OUTER JOIN', + 'INNER JOIN', + 'JOIN', + ]; + if (in_array($this->value, $joins, true)) { + return [ + 'types' => [ + self::TOKEN_TYPE_RESERVED_TOPLEVEL, + self::TOKEN_TYPE_EOF, + ], + 'values' => $joins, + 'addNewline' => false, + ]; + } + + return false; + } + + public function isBlockEnd($condition) + { + if (isset($condition['types']) && $this->isOfType(...$condition['types'])) { + return true; + } + + if (isset($condition['values']) && in_array($this->value, $condition['values'], true)) { + return true; + } + + return false; + } } diff --git a/src/Tokenizer.php b/src/Tokenizer.php index d5e75cd..46314c5 100644 --- a/src/Tokenizer.php +++ b/src/Tokenizer.php @@ -189,7 +189,6 @@ final class Tokenizer 'NOW()', 'NULL', 'OFFSET', - 'ON', 'OPEN', 'OPTIMIZE', 'OPTION', @@ -358,6 +357,7 @@ final class Tokenizer 'OR', 'AND', 'EXCLUDE', + 'ON', ]; /** @var string[] */ @@ -922,7 +922,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED_NEWLINE, - substr($string, 0, strlen($matches[1])) + preg_replace('/\s+/', ' ', substr($string, 0, strlen($matches[1]))) ); } diff --git a/tests/format.html b/tests/format.html index 7d946fc..75eb65f 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 ( diff --git a/tests/sql.sql b/tests/sql.sql index 620ad93..ca14a64 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; From 108d509753bf86b1bc6a790fc9cbd373b6373864 Mon Sep 17 00:00:00 2001 From: Rauno Moisto Date: Mon, 14 Aug 2023 18:08:26 +0300 Subject: [PATCH 2/9] Added Condition class --- src/Condition.php | 23 +++++++++++++++++++++++ src/SqlFormatter.php | 29 ++++++++++++----------------- src/Token.php | 28 ++++++++++++---------------- 3 files changed, 47 insertions(+), 33 deletions(-) create mode 100644 src/Condition.php 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 @@ +tokenizer->tokenize($string); @@ -120,13 +118,10 @@ public function format(string $string, string $indentString = ' '): string $last = end($expectedBlockEnds); $prev = prev($expectedBlockEnds); - if ($token->isBlockEnd($prev) && !in_array(Token::TOKEN_TYPE_EOF, ($prev['types'] ?? []), true)) { - if (isset($last['types']) && in_array(Token::TOKEN_TYPE_EOF, ($last['types'] ?? []), true)) { - // TODO loop instead? - array_pop($expectedBlockEnds); - array_pop($indentTypes); - $indentLevel--; - } + if ($last->eof && ! $prev->eof && $token->isBlockEnd($prev)) { + array_pop($expectedBlockEnds); + array_pop($indentTypes); + $indentLevel--; } } @@ -220,7 +215,7 @@ public function format(string $string, string $indentString = ' '): string if (! $inlineBlock) { $increaseBlockIndent = true; // Add a newline after the block - if ($newBlockEndCondition['addNewline']) { + if ($newBlockEndCondition->addNewline) { $newline = true; } } @@ -250,7 +245,7 @@ public function format(string $string, string $indentString = ' '): string } // Add a newline before the closing block (if not already added) - if (! $addedNewline && $blockEndCondition['addNewline']) { + if (! $addedNewline && $blockEndCondition->addNewline) { $return .= "\n" . str_repeat($tab, $indentLevel); } } @@ -293,7 +288,7 @@ public function format(string $string, string $indentString = ' '): string $clauseLimit = false; } 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 the previous TOKEN_VALUE is 'LIMIT', resets new line if ($clauseLimit === true) { $newline = false; $clauseLimit = false; @@ -369,7 +364,7 @@ public function format(string $string, string $indentString = ' '): string } $blockEndCondition = end($expectedBlockEnds); - if ($blockEndCondition && in_array(Token::TOKEN_TYPE_EOF, $blockEndCondition['types'] ?? [], true)) { + if ($blockEndCondition && $blockEndCondition->eof) { array_pop($expectedBlockEnds); } diff --git a/src/Token.php b/src/Token.php index 557030d..6724902 100644 --- a/src/Token.php +++ b/src/Token.php @@ -23,7 +23,6 @@ final class Token public const TOKEN_TYPE_NUMBER = 10; public const TOKEN_TYPE_ERROR = 11; public const TOKEN_TYPE_VARIABLE = 12; - public const TOKEN_TYPE_EOF = 13; // Constants for different components of a token public const TOKEN_TYPE = 0; @@ -70,11 +69,12 @@ public function withValue(string $value): self public function isBlockStart() { + $condition = new Condition(); + if ($this->value === '(') { - return [ - 'values' => [')'], - 'addNewline' => true, - ]; + $condition->values = [')']; + $condition->addNewline = true; + return $condition; } $joins = [ @@ -87,26 +87,22 @@ public function isBlockStart() 'JOIN', ]; if (in_array($this->value, $joins, true)) { - return [ - 'types' => [ - self::TOKEN_TYPE_RESERVED_TOPLEVEL, - self::TOKEN_TYPE_EOF, - ], - 'values' => $joins, - 'addNewline' => false, - ]; + $condition->values = $joins; + $condition->types = [self::TOKEN_TYPE_RESERVED_TOPLEVEL]; + $condition->eof = true; + return $condition; } return false; } - public function isBlockEnd($condition) + public function isBlockEnd(Condition $condition): bool { - if (isset($condition['types']) && $this->isOfType(...$condition['types'])) { + if ($this->isOfType(...$condition->types)) { return true; } - if (isset($condition['values']) && in_array($this->value, $condition['values'], true)) { + if (in_array($this->value, $condition->values, true)) { return true; } From bdc5b23fef958a86a0ca09d5549c8158c7dfe509 Mon Sep 17 00:00:00 2001 From: Rauno Moisto Date: Mon, 14 Aug 2023 19:30:14 +0300 Subject: [PATCH 3/9] Cleanup --- src/SqlFormatter.php | 27 +++++++-------------------- src/Token.php | 18 ++++++++---------- src/Tokenizer.php | 7 ++++--- tests/clihighlight.html | 26 +++++++++++++++++--------- tests/compress.html | 2 +- tests/format-highlight.html | 26 +++++++++++++++++--------- tests/highlight.html | 7 +++---- 7 files changed, 57 insertions(+), 56 deletions(-) diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php index ee42aca..a5b8d03 100644 --- a/src/SqlFormatter.php +++ b/src/SqlFormatter.php @@ -12,8 +12,10 @@ namespace Doctrine\SqlFormatter; use function array_pop; -use function assert; +use function count; +use function end; use function preg_replace; +use function prev; use function rtrim; use function str_repeat; use function str_replace; @@ -24,7 +26,7 @@ final class SqlFormatter { - private const INDENT_BLOCK = 1; + private const INDENT_BLOCK = 1; private const INDENT_SPECIAL = 2; /** @var Highlighter */ @@ -81,7 +83,7 @@ public function format(string $string, string $indentString = ' '): string if ($increaseSpecialIndent) { $indentLevel++; $increaseSpecialIndent = false; - $indentTypes[] = self::INDENT_SPECIAL; + $indentTypes[] = self::INDENT_SPECIAL; } // If we are increasing the block indent level now @@ -118,7 +120,7 @@ public function format(string $string, string $indentString = ' '): string $last = end($expectedBlockEnds); $prev = prev($expectedBlockEnds); - if ($last->eof && ! $prev->eof && $token->isBlockEnd($prev)) { + if ($prev && $last->eof && ! $prev->eof && $token->isBlockEnd($prev)) { array_pop($expectedBlockEnds); array_pop($indentTypes); $indentLevel--; @@ -159,7 +161,7 @@ public function format(string $string, string $indentString = ' '): string $newBlockEndCondition = $token->isBlockStart(); // Start of new block, increase the indent level and start a new line - if ($newBlockEndCondition !== false) { + 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 block @@ -424,21 +426,6 @@ public function compress(string $string): string continue; } - // Remove extra whitespace in reserved words (e.g "OUTER JOIN" becomes "OUTER JOIN") - // TODO move to Tokenizer - - 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 6724902..79de035 100644 --- a/src/Token.php +++ b/src/Token.php @@ -67,13 +67,14 @@ public function withValue(string $value): self return new self($this->type(), $value); } - public function isBlockStart() + public function isBlockStart(): ?Condition { $condition = new Condition(); if ($this->value === '(') { - $condition->values = [')']; + $condition->values = [')']; $condition->addNewline = true; + return $condition; } @@ -88,12 +89,13 @@ public function isBlockStart() ]; if (in_array($this->value, $joins, true)) { $condition->values = $joins; - $condition->types = [self::TOKEN_TYPE_RESERVED_TOPLEVEL]; - $condition->eof = true; + $condition->types = [self::TOKEN_TYPE_RESERVED_TOPLEVEL]; + $condition->eof = true; + return $condition; } - return false; + return null; } public function isBlockEnd(Condition $condition): bool @@ -102,10 +104,6 @@ public function isBlockEnd(Condition $condition): bool return true; } - if (in_array($this->value, $condition->values, true)) { - return true; - } - - return false; + return in_array($this->value, $condition->values, true); } } diff --git a/src/Tokenizer.php b/src/Tokenizer.php index 46314c5..fc6c6bb 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; @@ -908,7 +909,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED_TOPLEVEL, - substr($string, 0, strlen($matches[1])) + (string) preg_replace('/\s+/', ' ', substr($string, 0, strlen($matches[1]))) ); } @@ -922,7 +923,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED_NEWLINE, - preg_replace('/\s+/', ' ', substr($string, 0, strlen($matches[1]))) + (string) preg_replace('/\s+/', ' ', substr($string, 0, strlen($matches[1]))) ); } @@ -936,7 +937,7 @@ private function createNextToken(string $string, ?Token $previous = null): Token ) { return new Token( Token::TOKEN_TYPE_RESERVED, - substr($string, 0, strlen($matches[1])) + (string) preg_replace('/\s+/', ' ', substr($string, 0, strlen($matches[1]))) ); } } diff --git a/tests/clihighlight.html b/tests/clihighlight.html index 3253ff4..2c564f7 100755 --- a/tests/clihighlight.html +++ b/tests/clihighlight.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 ( diff --git a/tests/compress.html b/tests/compress.html index 311a904..62e3870 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 --- diff --git a/tests/format-highlight.html b/tests/format-highlight.html index ec1b2f1..a8f5310 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 (
diff --git a/tests/highlight.html b/tests/highlight.html
index 70516e5..255cb5f 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 (

From ec7f3243bd26a75c18eb423e0ecda9318e188ded Mon Sep 17 00:00:00 2001
From: Rauno Moisto 
Date: Tue, 15 Aug 2023 11:33:05 +0300
Subject: [PATCH 4/9] Spaces cleanup

---
 src/SqlFormatter.php | 78 +++++++++++++++++---------------------------
 src/Token.php        | 17 +++++-----
 2 files changed, 39 insertions(+), 56 deletions(-)

diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php
index a5b8d03..b8ff717 100644
--- a/src/SqlFormatter.php
+++ b/src/SqlFormatter.php
@@ -14,7 +14,6 @@
 use function array_pop;
 use function count;
 use function end;
-use function preg_replace;
 use function prev;
 use function rtrim;
 use function str_repeat;
@@ -95,7 +94,6 @@ public function format(string $string, string $indentString = '  '): string
             // If we need a new line before the token
             $addedNewline = false;
             if ($newline) {
-                $return       = rtrim($return, ' ');
                 $return      .= "\n" . str_repeat($tab, $indentLevel);
                 $newline      = false;
                 $addedNewline = true;
@@ -133,18 +131,22 @@ public function format(string $string, string $indentString = '  '): string
                 // End of inline block
                 if ($blockEndCondition && $token->isBlockEnd($blockEndCondition)) {
                     array_pop($expectedBlockEnds);
-                    $return = rtrim($return, ' ');
 
                     if ($inlineIndented) {
                         array_pop($indentTypes);
                         $indentLevel--;
-                        $return  = rtrim($return, ' ');
                         $return .= "\n" . str_repeat($tab, $indentLevel);
                     }
 
                     $inlineBlock = false;
 
-                    $return .= $highlighted . ' ';
+                    $return .= $highlighted;
+
+                    $nextNotWhitespace = $cursor->subCursor()->next(Token::TOKEN_TYPE_WHITESPACE);
+                    if ($nextNotWhitespace && $nextNotWhitespace->wantsSpaceBefore()) {
+                        $return .= ' ';
+                    }
+
                     continue;
                 }
 
@@ -231,9 +233,6 @@ public function format(string $string, string $indentString = '  '): string
 
             if ($blockEndCondition && $token->isBlockEnd($blockEndCondition)) {
                 // Closing block decrease the block indent level
-                // Remove whitespace before the closing block
-                $return = rtrim($return, ' ');
-
                 array_pop($expectedBlockEnds);
                 $indentLevel--;
 
@@ -266,17 +265,12 @@ public function format(string $string, string $indentString = '  '): string
                 $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' && ! $inlineBlock) {
                     $clauseLimit = true;
@@ -302,13 +296,8 @@ public function format(string $string, string $indentString = '  '): string
                 // 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);
@@ -320,49 +309,43 @@ public function format(string $string, string $indentString = '  '): string
                 }
             }
 
-            // 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 (
-                $prev->isOfType(
-                    Token::TOKEN_TYPE_QUOTE,
-                    Token::TOKEN_TYPE_BACKTICK_QUOTE,
-                    Token::TOKEN_TYPE_WORD,
-                    Token::TOKEN_TYPE_NUMBER
-                )
-            ) {
-                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;
+                }
             }
 
-            $return = rtrim($return, ' ');
+            $return .= ' ';
         }
 
         $blockEndCondition = end($expectedBlockEnds);
@@ -372,7 +355,6 @@ public function format(string $string, string $indentString = '  '): string
 
         // If there are unmatched blocks
         if (count($expectedBlockEnds)) {
-            $return  = rtrim($return, ' ');
             $return .= $this->highlighter->highlightErrorMessage(
                 'WARNING: unclosed block'
             );
diff --git a/src/Token.php b/src/Token.php
index 79de035..fab29b6 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,13 +54,6 @@ public function isOfType(int ...$types): bool
         return in_array($this->type, $types, true);
     }
 
-    public function hasExtraWhitespace(): bool
-    {
-        return strpos($this->value(), ' ') !== false ||
-            strpos($this->value(), "\n") !== false ||
-            strpos($this->value(), "\t") !== false;
-    }
-
     public function withValue(string $value): self
     {
         return new self($this->type(), $value);
@@ -106,4 +98,13 @@ public function isBlockEnd(Condition $condition): bool
 
         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);
+    }
 }

From 6c50b003b4aba20c449ffcc44e8dc4a08ca00b2d Mon Sep 17 00:00:00 2001
From: Rauno Moisto 
Date: Tue, 15 Aug 2023 12:57:32 +0300
Subject: [PATCH 5/9] Added CASE WHEN block, fixed comments

---
 src/SqlFormatter.php | 28 ++++++++++++++++++++++------
 src/Token.php        | 19 ++++++++++++++++++-
 src/Tokenizer.php    |  4 ++++
 3 files changed, 44 insertions(+), 7 deletions(-)

diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php
index b8ff717..2e34f5b 100644
--- a/src/SqlFormatter.php
+++ b/src/SqlFormatter.php
@@ -16,6 +16,7 @@
 use function end;
 use function prev;
 use function rtrim;
+use function str_contains;
 use function str_repeat;
 use function str_replace;
 use function strlen;
@@ -101,15 +102,30 @@ public function format(string $string, string $indentString = '  '): string
 
             // Display comments directly where they appear in the source
             if ($token->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 && str_contains($prev->value(), "\n") && ! $addedNewline) {
+                        $return .= "\n" . $indent;
+                    } elseif (! $addedNewline) {
+                        $return .= ' ';
+                    }
+                }
+
+                if ($isBlockComment) {
+                    $return .= str_replace("\n", "\n" . $indent, $highlighted);
+                    continue;
                 }
 
                 $return .= $highlighted;
-                $newline = true;
                 continue;
             }
 
diff --git a/src/Token.php b/src/Token.php
index fab29b6..d4c6d77 100644
--- a/src/Token.php
+++ b/src/Token.php
@@ -70,6 +70,18 @@ public function isBlockStart(): ?Condition
             return $condition;
         }
 
+        if ($this->value === 'THEN') {
+            $condition->values = ['ELSE', 'END'];
+
+            return $condition;
+        }
+
+        if ($this->value === 'ELSE') {
+            $condition->values = ['END'];
+
+            return $condition;
+        }
+
         $joins = [
             'LEFT OUTER JOIN',
             'RIGHT OUTER JOIN',
@@ -105,6 +117,11 @@ public function wantsSpaceBefore(): bool
             return false;
         }
 
-        return ! $this->isOfType(self::TOKEN_TYPE_RESERVED_NEWLINE, self::TOKEN_TYPE_RESERVED_TOPLEVEL);
+        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 fc6c6bb..cd8fe0b 100644
--- a/src/Tokenizer.php
+++ b/src/Tokenizer.php
@@ -359,6 +359,10 @@ final class Tokenizer
         'AND',
         'EXCLUDE',
         'ON',
+        'CASE WHEN',
+        'THEN',
+        'ELSE',
+        'END',
     ];
 
     /** @var string[] */

From 704b1bd7d18a522b76724a6e1713cae7868acdc6 Mon Sep 17 00:00:00 2001
From: Rauno Moisto 
Date: Tue, 15 Aug 2023 15:01:03 +0300
Subject: [PATCH 6/9] Fixed operators, added test case

---
 src/SqlFormatter.php        |  19 ++++---
 src/Tokenizer.php           |   3 -
 tests/clihighlight.html     | 107 ++++++++++++++++++++++++++++++++++++
 tests/compress.html         |   2 +
 tests/format-highlight.html | 107 ++++++++++++++++++++++++++++++++++++
 tests/format.html           | 107 ++++++++++++++++++++++++++++++++++++
 tests/highlight.html        |  76 +++++++++++++++++++++++++
 tests/sql.sql               |  76 +++++++++++++++++++++++++
 8 files changed, 485 insertions(+), 12 deletions(-)

diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php
index 2e34f5b..93f759d 100644
--- a/src/SqlFormatter.php
+++ b/src/SqlFormatter.php
@@ -14,6 +14,7 @@
 use function array_pop;
 use function count;
 use function end;
+use function in_array;
 use function prev;
 use function rtrim;
 use function str_contains;
@@ -314,15 +315,6 @@ public function format(string $string, string $indentString = '  '): string
                 if (! $addedNewline) {
                     $return .= "\n" . str_repeat($tab, $indentLevel);
                 }
-            } 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, ' ');
-                    }
-                }
             }
 
             $return .= $highlighted;
@@ -361,6 +353,15 @@ public function format(string $string, string $indentString = '  '): string
                 }
             }
 
+            // Don't add whitespace between operators like != <> >= := && etc.
+            if (
+                $token->isOfType(Token::TOKEN_TYPE_BOUNDARY)
+                && $nextNotWhitespace->isOfType(Token::TOKEN_TYPE_BOUNDARY)
+                && ! in_array($nextNotWhitespace->value(), ['(', '-'], true)
+            ) {
+                continue;
+            }
+
             $return .= ' ';
         }
 
diff --git a/src/Tokenizer.php b/src/Tokenizer.php
index cd8fe0b..e3a04cb 100644
--- a/src/Tokenizer.php
+++ b/src/Tokenizer.php
@@ -93,9 +93,7 @@ final class Tokenizer
         'DUMPFILE',
         'DUPLICATE',
         'DYNAMIC',
-        'ELSE',
         'ENCLOSED',
-        'END',
         'ENGINE',
         'ENGINE_TYPE',
         'ENGINES',
@@ -284,7 +282,6 @@ final class Tokenizer
         'TABLES',
         'TEMPORARY',
         'TERMINATED',
-        'THEN',
         'TIES',
         'TO',
         'TRAILING',
diff --git a/tests/clihighlight.html b/tests/clihighlight.html
index 2c564f7..fb8b749 100755
--- a/tests/clihighlight.html
+++ b/tests/clihighlight.html
@@ -983,3 +983,110 @@
     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
+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
+  )
+HAVING
+  new_service = 'Old'
+  -- Skip Old -> Old service
+  AND old_service <> 'Old'
+ORDER BY
+  old_service,
+  r.name ASC
diff --git a/tests/compress.html b/tests/compress.html
index 62e3870..6c2499c 100755
--- a/tests/compress.html
+++ b/tests/compress.html
@@ -91,3 +91,5 @@
 WITH cte1 AS (SELECT a, b FROM table1), cte2 AS (SELECT c, d FROM table2) SELECT b, d FROM cte1 JOIN cte2 WHERE cte1.a = cte2.c;
 ---
 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 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) HAVING new_service = 'Old' AND old_service <> 'Old' ORDER BY old_service, r.name ASC
diff --git a/tests/format-highlight.html b/tests/format-highlight.html
index a8f5310..c193129 100755
--- a/tests/format-highlight.html
+++ b/tests/format-highlight.html
@@ -983,3 +983,110 @@
     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
+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
+  )
+HAVING
+  new_service = 'Old'
+  -- Skip Old -> Old service
+  AND old_service <> 'Old'
+ORDER BY
+  old_service,
+  r.name ASC
diff --git a/tests/format.html b/tests/format.html index 75eb65f..179d0d7 100755 --- a/tests/format.html +++ b/tests/format.html @@ -982,3 +982,110 @@ 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 +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 + ) +HAVING + new_service = 'Old' + -- Skip Old -> Old service + AND old_service <> 'Old' +ORDER BY + old_service, + r.name ASC diff --git a/tests/highlight.html b/tests/highlight.html index 255cb5f..6f61b64 100755 --- a/tests/highlight.html +++ b/tests/highlight.html @@ -301,3 +301,79 @@ 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
+    -- 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
+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)
+HAVING
+  new_service = 'Old'
+  -- Skip Old -> Old service
+  AND old_service <> 'Old'
+ORDER BY
+  old_service,
+  r.name ASC
diff --git a/tests/sql.sql b/tests/sql.sql index ca14a64..e612689 100755 --- a/tests/sql.sql +++ b/tests/sql.sql @@ -303,3 +303,79 @@ SELECT a, 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 + -- 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 +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) +HAVING + new_service = 'Old' + -- Skip Old -> Old service + AND old_service <> 'Old' +ORDER BY + old_service, + r.name ASC From 90c215bdab2fe9bbb00984c381250f165762553f Mon Sep 17 00:00:00 2001 From: Rauno Moisto Date: Tue, 15 Aug 2023 17:30:27 +0300 Subject: [PATCH 7/9] Fixed more CASEs, ON DUPLICATE KEY UPDATE, added tests --- src/SqlFormatter.php | 10 ++++++++++ src/Token.php | 4 ++-- src/Tokenizer.php | 7 ++++--- tests/clihighlight.html | 21 ++++++++++++++++----- tests/compress.html | 4 +++- tests/format-highlight.html | 21 ++++++++++++++++----- tests/format.html | 21 ++++++++++++++++----- tests/highlight.html | 13 ++++++++++--- tests/sql.sql | 9 ++++++++- 9 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php index 93f759d..9c990c3 100644 --- a/src/SqlFormatter.php +++ b/src/SqlFormatter.php @@ -269,6 +269,15 @@ public function format(string $string, string $indentString = ' '): string } 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; + } + } + // Top level reserved words start a new line and increase the special indent level $increaseSpecialIndent = true; @@ -356,6 +365,7 @@ public function format(string $string, string $indentString = ' '): string // Don't add whitespace between operators like != <> >= := && etc. if ( $token->isOfType(Token::TOKEN_TYPE_BOUNDARY) + && $token->value() !== ')' && $nextNotWhitespace->isOfType(Token::TOKEN_TYPE_BOUNDARY) && ! in_array($nextNotWhitespace->value(), ['(', '-'], true) ) { diff --git a/src/Token.php b/src/Token.php index d4c6d77..bf8ebd8 100644 --- a/src/Token.php +++ b/src/Token.php @@ -70,13 +70,13 @@ public function isBlockStart(): ?Condition return $condition; } - if ($this->value === 'THEN') { + if ($this->value === 'CASE WHEN' || $this->value === 'ELSE') { $condition->values = ['ELSE', 'END']; return $condition; } - if ($this->value === 'ELSE') { + if ($this->value === 'CASE') { $condition->values = ['END']; return $condition; diff --git a/src/Tokenizer.php b/src/Tokenizer.php index e3a04cb..d476417 100644 --- a/src/Tokenizer.php +++ b/src/Tokenizer.php @@ -50,7 +50,6 @@ final class Tokenizer 'BINLOG', 'BOTH', 'CASCADE', - 'CASE', 'CHANGE', 'CHANGED', 'CHARACTER SET', @@ -300,7 +299,6 @@ final class Tokenizer 'USING', 'VARIABLES', 'VIEW', - 'WHEN', 'WITH', 'WORK', 'WRITE', @@ -340,6 +338,7 @@ final class Tokenizer 'RANGE', 'GROUPS', 'WINDOW', + 'ON DUPLICATE KEY UPDATE', ]; /** @var string[] */ @@ -357,7 +356,8 @@ final class Tokenizer 'EXCLUDE', 'ON', 'CASE WHEN', - 'THEN', + 'CASE', + 'WHEN', 'ELSE', 'END', ]; @@ -671,6 +671,7 @@ final class Tokenizer 'UTC_TIME', 'UTC_TIMESTAMP', 'UUID', + 'VALUES', 'VAR', 'VARIANCE', 'VARP', diff --git a/tests/clihighlight.html b/tests/clihighlight.html index fb8b749..d34c2a5 100755 --- a/tests/clihighlight.html +++ b/tests/clihighlight.html @@ -987,8 +987,7 @@ SELECT SQL_NO_CACHE r.name, r.time, - CASE WHEN r.message LIKE 'old_service %' - THEN + CASE WHEN r.message LIKE 'old_service %' THEN -- Extract old service name from service message SUBSTRING_INDEX( SUBSTRING_INDEX( @@ -1007,8 +1006,7 @@ 1 ) END old_service, - CASE WHEN r.message LIKE 'service %' - THEN + CASE WHEN r.message LIKE 'service %' THEN -- Extract service name from service message SUBSTRING_INDEX( SUBSTRING_INDEX( @@ -1023,7 +1021,11 @@ -- Extract service name from other message 'Default' END new_service, - p.name current_service + p.name current_service, + CASE r.name + WHEN 'pre-fix' THEN 'pre' + WHEN 'post-fix' THEN 'post' + END AS fix FROM ( SELECT @@ -1083,6 +1085,9 @@ p.type = 1 OR p.id = 7 ) + AND MD5( + CONCAT(p.type, ?) + ) = '11' HAVING new_service = 'Old' -- Skip Old -> Old service @@ -1090,3 +1095,9 @@ ORDER BY old_service, r.name ASC +--- +INSERT INTO customers (id, username) +VALUES + (1, 'Joe') +ON DUPLICATE KEY UPDATE + username = VALUES(username) diff --git a/tests/compress.html b/tests/compress.html index 6c2499c..ae55dbe 100755 --- a/tests/compress.html +++ b/tests/compress.html @@ -92,4 +92,6 @@ --- 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 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) HAVING new_service = 'Old' AND old_service <> 'Old' ORDER BY old_service, r.name ASC +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) diff --git a/tests/format-highlight.html b/tests/format-highlight.html index c193129..ed5ec9f 100755 --- a/tests/format-highlight.html +++ b/tests/format-highlight.html @@ -987,8 +987,7 @@
SELECT
   SQL_NO_CACHE r.name,
   r.time,
-  CASE WHEN r.message LIKE 'old_service %'
-  THEN
+  CASE WHEN r.message LIKE 'old_service %' THEN
     -- Extract old service name from service message
     SUBSTRING_INDEX(
       SUBSTRING_INDEX(
@@ -1007,8 +1006,7 @@
       1
     )
   END old_service,
-  CASE WHEN r.message LIKE 'service %'
-  THEN
+  CASE WHEN r.message LIKE 'service %' THEN
     -- Extract service name from service message
     SUBSTRING_INDEX(
       SUBSTRING_INDEX(
@@ -1023,7 +1021,11 @@
     -- Extract service name from other message
     'Default'
   END new_service,
-  p.name current_service
+  p.name current_service,
+  CASE r.name
+    WHEN 'pre-fix' THEN 'pre'
+    WHEN 'post-fix' THEN 'post'
+  END AS fix
 FROM
   (
     SELECT
@@ -1083,6 +1085,9 @@
     p.type = 1
     OR p.id = 7
   )
+  AND MD5(
+    CONCAT(p.type, ?)
+  ) = '11'
 HAVING
   new_service = 'Old'
   -- Skip Old -> Old service
@@ -1090,3 +1095,9 @@
 ORDER BY
   old_service,
   r.name ASC
+--- +
INSERT INTO customers (id, username)
+VALUES
+  (1, 'Joe')
+ON DUPLICATE KEY UPDATE
+  username = VALUES(username)
diff --git a/tests/format.html b/tests/format.html index 179d0d7..a3ecd27 100755 --- a/tests/format.html +++ b/tests/format.html @@ -986,8 +986,7 @@ SELECT SQL_NO_CACHE r.name, r.time, - CASE WHEN r.message LIKE 'old_service %' - THEN + CASE WHEN r.message LIKE 'old_service %' THEN -- Extract old service name from service message SUBSTRING_INDEX( SUBSTRING_INDEX( @@ -1006,8 +1005,7 @@ 1 ) END old_service, - CASE WHEN r.message LIKE 'service %' - THEN + CASE WHEN r.message LIKE 'service %' THEN -- Extract service name from service message SUBSTRING_INDEX( SUBSTRING_INDEX( @@ -1022,7 +1020,11 @@ -- Extract service name from other message 'Default' END new_service, - p.name current_service + p.name current_service, + CASE r.name + WHEN 'pre-fix' THEN 'pre' + WHEN 'post-fix' THEN 'post' + END AS fix FROM ( SELECT @@ -1082,6 +1084,9 @@ p.type = 1 OR p.id = 7 ) + AND MD5( + CONCAT(p.type, ?) + ) = '11' HAVING new_service = 'Old' -- Skip Old -> Old service @@ -1089,3 +1094,9 @@ ORDER BY old_service, r.name ASC +--- +INSERT INTO customers (id, username) +VALUES + (1, 'Joe') +ON DUPLICATE KEY UPDATE + username = VALUES(username) diff --git a/tests/highlight.html b/tests/highlight.html index 6f61b64..ec53a60 100755 --- a/tests/highlight.html +++ b/tests/highlight.html @@ -305,21 +305,22 @@
SELECT
   SQL_NO_CACHE r.name,
   r.time,
-  CASE WHEN r.message LIKE 'old_service %' THEN
+  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
+  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
+  p.name current_service,
+  CASE r.name WHEN 'pre-fix' THEN 'pre' WHEN 'post-fix' THEN 'post' END AS fix
 FROM
   (
     SELECT
@@ -370,6 +371,7 @@
   -- 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
@@ -377,3 +379,8 @@
 ORDER BY
   old_service,
   r.name ASC
+--- +
INSERT INTO customers (id, username)
+VALUES (1, 'Joe')
+ON DUPLICATE KEY UPDATE
+username = VALUES(username)
diff --git a/tests/sql.sql b/tests/sql.sql index e612689..618d08f 100755 --- a/tests/sql.sql +++ b/tests/sql.sql @@ -321,7 +321,8 @@ SELECT -- Extract service name from other message 'Default' END new_service, - p.name current_service + p.name current_service, + CASE r.name WHEN 'pre-fix' THEN 'pre' WHEN 'post-fix' THEN 'post' END AS fix FROM ( SELECT @@ -372,6 +373,7 @@ WHERE -- 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 @@ -379,3 +381,8 @@ HAVING ORDER BY old_service, r.name ASC +--- +INSERT INTO customers (id, username) +VALUES (1, 'Joe') +ON DUPLICATE KEY UPDATE +username = VALUES(username) From b2217f5aa69a6c94832479effc19f90c8bd04af1 Mon Sep 17 00:00:00 2001 From: Rauno Moisto Date: Wed, 16 Aug 2023 09:23:08 +0300 Subject: [PATCH 8/9] PHP 7 compatibility --- src/SqlFormatter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SqlFormatter.php b/src/SqlFormatter.php index 9c990c3..401ef18 100644 --- a/src/SqlFormatter.php +++ b/src/SqlFormatter.php @@ -17,10 +17,10 @@ use function in_array; use function prev; use function rtrim; -use function str_contains; use function str_repeat; use function str_replace; use function strlen; +use function strpos; use function trim; use const PHP_SAPI; @@ -114,7 +114,7 @@ public function format(string $string, string $indentString = ' '): string } else { $prev = $cursor->subCursor()->previous(); // Single line comment wants to have a newline before - if ($prev && str_contains($prev->value(), "\n") && ! $addedNewline) { + if ($prev && ! $addedNewline && strpos($prev->value(), "\n") !== false) { $return .= "\n" . $indent; } elseif (! $addedNewline) { $return .= ' '; From 12ccdd1fd37d737201c9099c2501f545bf730867 Mon Sep 17 00:00:00 2001 From: Rauno Moisto Date: Wed, 16 Aug 2023 11:15:04 +0300 Subject: [PATCH 9/9] Fixed CROSS JOIN --- src/Token.php | 1 + src/Tokenizer.php | 1 + tests/clihighlight.html | 4 ++-- tests/format-highlight.html | 4 ++-- tests/format.html | 4 ++-- tests/highlight.html | 5 ++--- tests/sql.sql | 5 ++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Token.php b/src/Token.php index bf8ebd8..bb27a19 100644 --- a/src/Token.php +++ b/src/Token.php @@ -89,6 +89,7 @@ public function isBlockStart(): ?Condition 'RIGHT JOIN', 'OUTER JOIN', 'INNER JOIN', + 'CROSS JOIN', 'JOIN', ]; if (in_array($this->value, $joins, true)) { diff --git a/src/Tokenizer.php b/src/Tokenizer.php index d476417..3174ae7 100644 --- a/src/Tokenizer.php +++ b/src/Tokenizer.php @@ -349,6 +349,7 @@ final class Tokenizer 'RIGHT JOIN', 'OUTER JOIN', 'INNER JOIN', + 'CROSS JOIN', 'JOIN', 'XOR', 'OR', diff --git a/tests/clihighlight.html b/tests/clihighlight.html index d34c2a5..8d90260 100755 --- a/tests/clihighlight.html +++ b/tests/clihighlight.html @@ -1062,8 +1062,8 @@ ORDER BY name, time DESC - ) entries CROSS - JOIN ( + ) entries + CROSS JOIN ( SELECT @rn := 0, @id := -1 diff --git a/tests/format-highlight.html b/tests/format-highlight.html index ed5ec9f..7b3aced 100755 --- a/tests/format-highlight.html +++ b/tests/format-highlight.html @@ -1062,8 +1062,8 @@ ORDER BY name, time DESC - ) entries CROSS - JOIN ( + ) entries + CROSS JOIN ( SELECT @rn := 0, @id := -1 diff --git a/tests/format.html b/tests/format.html index a3ecd27..cdc5512 100755 --- a/tests/format.html +++ b/tests/format.html @@ -1061,8 +1061,8 @@ ORDER BY name, time DESC - ) entries CROSS - JOIN ( + ) entries + CROSS JOIN ( SELECT @rn := 0, @id := -1 diff --git a/tests/highlight.html b/tests/highlight.html index ec53a60..551123c 100755 --- a/tests/highlight.html +++ b/tests/highlight.html @@ -353,9 +353,8 @@ ORDER BY name, time DESC - ) entries CROSS - JOIN - ( + ) entries + CROSS JOIN ( SELECT @rn := 0, @id := -1 diff --git a/tests/sql.sql b/tests/sql.sql index 618d08f..db73a27 100755 --- a/tests/sql.sql +++ b/tests/sql.sql @@ -355,9 +355,8 @@ FROM ORDER BY name, time DESC - ) entries CROSS - JOIN - ( + ) entries + CROSS JOIN ( SELECT @rn := 0, @id := -1