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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/BufferedFileParseTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php declare(strict_types = 1);

namespace Nextras\MultiQueryParser;

use Iterator;
use Nextras\MultiQueryParser\Exception\RuntimeException;
use function fclose;
use function feof;
use function fopen;
use function fread;
use function preg_match;
use function strlen;
use function substr;


trait BufferedFileParseTrait
{
/**
* @param callable(array<int|string, string>): array{?string, ?string} $processMatch
* @return Iterator<int, string>
*/
private function parseFileBuffered(string $path, string $pattern, callable $processMatch): Iterator
{
$handle = @fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException("Cannot open file '$path'.");
}

try {
$buffer = '';
$offset = 0;
$eof = false;
$chunkSize = 65536; // 64 KiB

while (true) {
// Read more data if buffer is running low and file is not exhausted
while (!$eof && strlen($buffer) - $offset < $chunkSize) {
$chunk = fread($handle, $chunkSize);
if ($chunk === false || $chunk === '') {
$eof = feof($handle);
break;
}
$buffer .= $chunk;
$eof = feof($handle);
}

if ($offset >= strlen($buffer)) {
break;
}

if (preg_match($pattern, $buffer, $match, 0, $offset) !== 1) {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffering logic will fail for queries larger than 64 KiB. After reading the first chunk (line 38), if the remaining buffer is >= chunkSize (line 37), the inner loop exits. If preg_match then fails because the query is incomplete (no delimiter found yet), the outer loop breaks (line 52), and a RuntimeException is thrown (line 92).

The fix should ensure that if preg_match fails and we're not at EOF, we continue reading more data instead of breaking. One approach would be to:

  1. Always try to read at least one more chunk after a failed match (if not at EOF)
  2. Or, change the inner loop condition to ensure we have enough buffer data to match complete patterns

This is critical because the PR's goal is to handle large files without loading them entirely into memory, but it fails for any individual query exceeding the chunk size.

Suggested change
if (preg_match($pattern, $buffer, $match, 0, $offset) !== 1) {
if (preg_match($pattern, $buffer, $match, 0, $offset) !== 1) {
// If no match is found but we're not at EOF, try to read more data
if (!$eof) {
$chunk = fread($handle, $chunkSize);
if ($chunk !== false && $chunk !== '') {
$buffer .= $chunk;
$eof = feof($handle);
continue; // retry matching with more data in the buffer
}
$eof = feof($handle);
}

Copilot uses AI. Check for mistakes.
break;
}

$matchEnd = $offset + strlen($match[0]);

// Safety check: if the match reaches the end of the buffer and we're not at EOF,
// read more data and retry — prevents \z from falsely matching at a chunk boundary
if ($matchEnd >= strlen($buffer) && !$eof) {
$chunk = fread($handle, $chunkSize);
if ($chunk !== false && $chunk !== '') {
$buffer .= $chunk;
$eof = feof($handle);
continue; // retry the match with more data
}
$eof = true;
}

$offset = $matchEnd;

[$query, $newPattern] = $processMatch($match);

if ($newPattern !== null) {
$pattern = $newPattern;
}

if ($query !== null) {
yield $query;
} elseif ($newPattern === null) {
// No query and no pattern change means we hit the \z end-of-content branch
break;
}

// Trim consumed content from the buffer to free memory
if ($offset > $chunkSize) {
$buffer = substr($buffer, $offset);
$offset = 0;
}
}

if ($offset !== strlen($buffer)) {
throw new RuntimeException("Failed to parse file '$path', please report an issue.");
}
} finally {
fclose($handle);
}
}
}
39 changes: 13 additions & 26 deletions src/MySqlMultiQueryParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,27 @@
namespace Nextras\MultiQueryParser;

use Iterator;
use Nextras\MultiQueryParser\Exception\RuntimeException;
use function file_get_contents;
use function preg_match;
use function preg_quote;
use function strlen;


class MySqlMultiQueryParser implements IMultiQueryParser
{
public function parseFile(string $path): Iterator
{
$content = @file_get_contents($path);
if ($content === false) {
throw new RuntimeException("Cannot open file '$path'.");
}

$offset = 0;
$pattern = $this->getQueryPattern(';');
use BufferedFileParseTrait;

while (preg_match($pattern, $content, $match, 0, $offset) === 1) {
$offset += strlen($match[0]);

if (isset($match['delimiter']) && $match['delimiter'] !== '') {
$pattern = $this->getQueryPattern($match['delimiter']);
} elseif (isset($match['query']) && $match['query'] !== '') {
yield $match['query'];
} else {
break;
public function parseFile(string $path): Iterator
{
return $this->parseFileBuffered(
$path,
$this->getQueryPattern(';'),
function (array $match): array {
if (isset($match['delimiter']) && $match['delimiter'] !== '') {
return [null, $this->getQueryPattern($match['delimiter'])];
}
$query = (isset($match['query']) && $match['query'] !== '') ? $match['query'] : null;
return [$query, null];
}
}

if ($offset !== strlen($content)) {
throw new RuntimeException("Failed to parse file '$path', please report an issue.");
}
);
}


Expand Down
34 changes: 10 additions & 24 deletions src/PostgreSqlMultiQueryParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,23 @@
namespace Nextras\MultiQueryParser;

use Iterator;
use Nextras\MultiQueryParser\Exception\RuntimeException;
use function file_get_contents;
use function preg_match;
use function strlen;


class PostgreSqlMultiQueryParser implements IMultiQueryParser
{
public function parseFile(string $path): Iterator
{
$content = @file_get_contents($path);
if ($content === false) {
throw new RuntimeException("Cannot open file '$path'.");
}

$offset = 0;
$pattern = $this->getQueryPattern();
use BufferedFileParseTrait;

while (preg_match($pattern, $content, $match, 0, $offset)) {
$offset += strlen($match[0]);

if (isset($match['query']) && $match['query'] !== '') {
yield $match['query'];
} else {
break;
public function parseFile(string $path): Iterator
{
return $this->parseFileBuffered(
$path,
$this->getQueryPattern(),
static function (array $match): array {
$query = (isset($match['query']) && $match['query'] !== '') ? $match['query'] : null;
return [$query, null];
}
}

if ($offset !== strlen($content)) {
throw new RuntimeException("Failed to parse file '$path', please report an issue.");
}
);
}


Expand Down
34 changes: 10 additions & 24 deletions src/SqlServerMultiQueryParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,23 @@
namespace Nextras\MultiQueryParser;

use Iterator;
use Nextras\MultiQueryParser\Exception\RuntimeException;
use function file_get_contents;
use function preg_match;
use function strlen;


class SqlServerMultiQueryParser implements IMultiQueryParser
{
public function parseFile(string $path): Iterator
{
$content = @file_get_contents($path);
if ($content === false) {
throw new RuntimeException("Cannot open file '$path'.");
}

$offset = 0;
$pattern = $this->getQueryPattern();
use BufferedFileParseTrait;

while (preg_match($pattern, $content, $match, 0, $offset)) {
$offset += strlen($match[0]);

if (isset($match['query']) && $match['query'] !== '') {
yield $match['query'];
} else {
break;
public function parseFile(string $path): Iterator
{
return $this->parseFileBuffered(
$path,
$this->getQueryPattern(),
static function (array $match): array {
$query = (isset($match['query']) && $match['query'] !== '') ? $match['query'] : null;
return [$query, null];
}
}

if ($offset !== strlen($content)) {
throw new RuntimeException("Failed to parse file '$path', please report an issue.");
}
);
}


Expand Down