Skip to content

Commit 59363bc

Browse files
authored
Add CommentStrategy to optionally preserve leading comments (#53)
1 parent 13af8ca commit 59363bc

19 files changed

Lines changed: 540 additions & 44 deletions

readme.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,27 @@ foreach ($parser->parseFileStream($stream) as $query) {
6060

6161
Available parsers: `MySqlMultiQueryParser`, `PostgreSqlMultiQueryParser`, `SqlServerMultiQueryParser`, `SqliteMultiQueryParser`.
6262

63+
**Keep leading comments:**
64+
65+
By default, comments are stripped and only query strings are yielded. To control what happens to
66+
comments, pass a `CommentStrategy` to the parser constructor. The bundled `PrependLeadingComments`
67+
strategy keeps the comments preceding a query as a prefix of that query -- useful when comments
68+
carry meaningful annotations, e.g. so they remain visible in observability tools:
69+
70+
```php
71+
use Nextras\MultiQueryParser\Strategy\PrependLeadingComments;
72+
73+
$parser = new MySqlMultiQueryParser(new PrependLeadingComments());
74+
75+
$sql = "-- create the users table\nCREATE TABLE users (id INT);";
76+
77+
foreach ($parser->parseString($sql) as $query) {
78+
echo $query; // "-- create the users table\nCREATE TABLE users (id INT)"
79+
}
80+
```
81+
82+
All comment styles supported by the given dialect (`--`, `/* */`, and `#` for MySQL) that directly precede a query are preserved with their original formatting; only pure leading whitespace is stripped. A comment that sits between two queries is treated as preceding the following one. Comments not followed by any query (e.g. a trailing comment at the end of input) are dropped.
83+
6384
### License
6485

6586
MIT. See full [license](license.md).

src/BaseMultiQueryParser.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@
55
use ArrayIterator;
66
use Iterator;
77
use Nextras\MultiQueryParser\Exception\RuntimeException;
8+
use Nextras\MultiQueryParser\Fragment\Comment;
9+
use Nextras\MultiQueryParser\Fragment\Fragment;
10+
use Nextras\MultiQueryParser\Fragment\Query;
11+
use Nextras\MultiQueryParser\Strategy\DropComments;
812
use function feof;
913
use function fopen;
1014
use function fread;
1115

1216

1317
abstract class BaseMultiQueryParser implements IMultiQueryParser
1418
{
19+
private CommentStrategy $commentStrategy;
20+
21+
22+
public function __construct(?CommentStrategy $commentStrategy = null)
23+
{
24+
$this->commentStrategy = $commentStrategy ?? new DropComments();
25+
}
26+
27+
1528
/**
1629
* @param positive-int $chunkSize
1730
* @return Iterator<string>
@@ -52,7 +65,32 @@ public function parseString(string $s): Iterator
5265
* @param Iterator<string> $stream
5366
* @return Iterator<string>
5467
*/
55-
abstract public function parseStringStream(Iterator $stream): Iterator;
68+
public function parseStringStream(Iterator $stream): Iterator
69+
{
70+
return $this->commentStrategy->apply($this->parseStringStreamToFragments($stream));
71+
}
72+
73+
74+
/**
75+
* @param Iterator<string> $stream
76+
* @return Iterator<Fragment>
77+
*/
78+
abstract protected function parseStringStreamToFragments(Iterator $stream): Iterator;
79+
80+
81+
/**
82+
* @return Iterator<Fragment>
83+
*/
84+
protected function buildFragments(?string $leadingComments, ?string $query): Iterator
85+
{
86+
if ($leadingComments !== null && $leadingComments !== '') {
87+
yield new Comment($leadingComments);
88+
}
89+
90+
if ($query !== null && $query !== '') {
91+
yield new Query($query);
92+
}
93+
}
5694

5795

5896
/**

src/CommentStrategy.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\MultiQueryParser;
4+
5+
use Iterator;
6+
use Nextras\MultiQueryParser\Fragment\Fragment;
7+
8+
9+
/**
10+
* Decides what happens to the comments found in the parsed SQL stream.
11+
*
12+
* The parsers themselves only tokenize the input into a neutral {@see Fragment} stream of queries
13+
* and comments; the strategy collapses that stream into the final stream of query strings.
14+
*/
15+
interface CommentStrategy
16+
{
17+
/**
18+
* @param Iterator<Fragment> $fragments
19+
* @return Iterator<string>
20+
*/
21+
public function apply(Iterator $fragments): Iterator;
22+
}

src/Fragment/Comment.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\MultiQueryParser\Fragment;
4+
5+
6+
/**
7+
* A run of one or more comments (and the whitespace interleaved with them).
8+
*/
9+
final class Comment implements Fragment
10+
{
11+
public function __construct(
12+
public string $text,
13+
) {
14+
}
15+
}

src/Fragment/Fragment.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\MultiQueryParser\Fragment;
4+
5+
6+
/**
7+
* @phpstan-sealed Query|Comment
8+
*/
9+
interface Fragment
10+
{
11+
}

src/Fragment/Query.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\MultiQueryParser\Fragment;
4+
5+
6+
/**
7+
* A single SQL query (without its terminating delimiter).
8+
*/
9+
final class Query implements Fragment
10+
{
11+
public function __construct(
12+
public string $sql,
13+
) {
14+
}
15+
}

src/MySqlMultiQueryParser.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@
88

99
class MySqlMultiQueryParser extends BaseMultiQueryParser
1010
{
11-
public function parseStringStream(Iterator $stream): Iterator
11+
protected function parseStringStreamToFragments(Iterator $stream): Iterator
1212
{
1313
$patternIterator = new PatternIterator($stream, $this->getQueryPattern(';'));
1414

1515
foreach ($patternIterator as $match) {
16+
yield from $this->buildFragments($match['leadingComments'] ?? null, $match['query'] ?? null);
17+
1618
if (isset($match['delimiter']) && $match['delimiter'] !== '') {
1719
$patternIterator->setPattern($this->getQueryPattern($match['delimiter']));
18-
19-
} elseif (isset($match['query']) && $match['query'] !== '') {
20-
yield $match['query'];
2120
}
2221
}
2322
}
@@ -30,12 +29,15 @@ private function getQueryPattern(string $delimiter): string
3029

3130
return /** @lang PhpRegExp */ "
3231
~
33-
(?:
34-
\\s
35-
| /\\* (*PRUNE) (?: [^*]++ | \\*(?!/) )*+ \\*/
36-
| --[^\\n]*+(?:\\n|\\z)
37-
| \\#[^\\n]*+(?:\\n|\\z)
38-
)*+
32+
\\s*+
33+
(?<leadingComments>
34+
(?:
35+
\\s
36+
| /\\* (*PRUNE) (?: [^*]++ | \\*(?!/) )*+ \\*/
37+
| --[^\\n]*+(?:\\n|\\z)
38+
| \\#[^\\n]*+(?:\\n|\\z)
39+
)*+
40+
)
3941
4042
(?:
4143
(?i:

src/PatternIterator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* the regex engine commits to the construct — if the closing delimiter is missing (because
3131
* it is in a later chunk), the overall match fails, causing the iterator to load more data.
3232
*
33-
* @implements IteratorAggregate<int, array<mixed>>
33+
* @implements IteratorAggregate<int, array<array-key, string>>
3434
*/
3535
class PatternIterator implements IteratorAggregate
3636
{

src/PostgreSqlMultiQueryParser.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77

88
class PostgreSqlMultiQueryParser extends BaseMultiQueryParser
99
{
10-
public function parseStringStream(Iterator $stream): Iterator
10+
protected function parseStringStreamToFragments(Iterator $stream): Iterator
1111
{
1212
$patternIterator = new PatternIterator($stream, $this->getQueryPattern());
1313

1414
foreach ($patternIterator as $match) {
15-
if (isset($match['query']) && $match['query'] !== '') {
16-
yield $match['query'];
17-
}
15+
yield from $this->buildFragments($match['leadingComments'] ?? null, $match['query'] ?? null);
1816
}
1917
}
2018

@@ -29,11 +27,14 @@ private function getQueryPattern(): string
2927
(?<nestedBc> /\\* (?: [^/*]++ | /(?!\\*) | \\*(?!/) | (?&nestedBc) )*+ \\*/ )
3028
)
3129
32-
(?:
33-
\\s
34-
| /\\* (*PRUNE) (?: [^/*]++ | /(?!\\*) | \\*(?!/) | (?&nestedBc) )*+ \\*/
35-
| -- [^\\n]*+
36-
)*+
30+
\\s*+
31+
(?<leadingComments>
32+
(?:
33+
\\s
34+
| /\\* (*PRUNE) (?: [^/*]++ | /(?!\\*) | \\*(?!/) | (?&nestedBc) )*+ \\*/
35+
| -- [^\\n]*+
36+
)*+
37+
)
3738
3839
(?:
3940
(?:

src/SqlServerMultiQueryParser.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77

88
class SqlServerMultiQueryParser extends BaseMultiQueryParser
99
{
10-
public function parseStringStream(Iterator $stream): Iterator
10+
protected function parseStringStreamToFragments(Iterator $stream): Iterator
1111
{
1212
$patternIterator = new PatternIterator($stream, $this->getQueryPattern());
1313

1414
foreach ($patternIterator as $match) {
15-
if (isset($match['query']) && $match['query'] !== '') {
16-
yield $match['query'];
17-
}
15+
yield from $this->buildFragments($match['leadingComments'] ?? null, $match['query'] ?? null);
1816
}
1917
}
2018

@@ -45,11 +43,14 @@ private function getQueryPattern(): string
4543
(?<nestedBc> /\\* (?: [^/*]++ | /(?!\\*) | \\*(?!/) | (?&nestedBc) )*+ \\*/ )
4644
)
4745
48-
(?:
49-
\\s
50-
| /\\* (*PRUNE) (?: [^/*]++ | /(?!\\*) | \\*(?!/) | (?&nestedBc) )*+ \\*/
51-
| -- [^\\n]*+
52-
)*+
46+
\\s*+
47+
(?<leadingComments>
48+
(?:
49+
\\s
50+
| /\\* (*PRUNE) (?: [^/*]++ | /(?!\\*) | \\*(?!/) | (?&nestedBc) )*+ \\*/
51+
| -- [^\\n]*+
52+
)*+
53+
)
5354
5455
(?:
5556
(?:

0 commit comments

Comments
 (0)