Skip to content

Fix ValueError when formatting procedures using label syntax (bar:LOOP)#185

Open
VCourdy wants to merge 1 commit into
doctrine:1.5.xfrom
VCourdy:fix/label-colon-loop-tokenization
Open

Fix ValueError when formatting procedures using label syntax (bar:LOOP)#185
VCourdy wants to merge 1 commit into
doctrine:1.5.xfrom
VCourdy:fix/label-colon-loop-tokenization

Conversation

@VCourdy

@VCourdy VCourdy commented Jul 2, 2026

Copy link
Copy Markdown

Bug

SqlFormatter::format() crashes with ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0 on MySQL stored procedures using label syntax with no space after the colon:

use Doctrine\SqlFormatter\NullHighlighter;
use Doctrine\SqlFormatter\SqlFormatter;

$stmt = 'CREATE PROCEDURE `foo`()
BEGIN
  bar:LOOP
    -- Nothing
  END LOOP bar;
END';

(new SqlFormatter(new NullHighlighter()))->format($stmt);
// ValueError in src/SqlFormatter.php:94

bar: LOOP (with a space) does not crash; the trigger is specifically : glued to LOOP.

Root cause

The tokenizer lexes : as a bind-variable prefix, so bar:LOOP tokenizes as WORD('bar') + VARIABLE(':LOOP'). The LOOP keyword is swallowed and the indent-opener branch in SqlFormatter::format() never fires, while END LOOP and the final END each still decrement the indentation level. It reaches -1 and str_repeat($tab, $indentLevel) throws.

Fix

A colon immediately preceded by a word character is never a bind-variable prefix in any supported dialect — it is a label separator (MySQL/Oracle label:LOOP). This PR guards the : alternative of the VARIABLE pattern with a negative lookbehind:

-Token::TOKEN_TYPE_VARIABLE => '[@:](?:...)',
+Token::TOKEN_TYPE_VARIABLE => '(?:@|(?<!\w):)(?:...)',

@ keeps its old unconditional behavior; only : gets the guard.

Impact on existing behavior

  • Zero changes to existing fixtures: all pre-existing sections of tests/sql.sql render byte-identically across all five expected-output files (only the new regression section is appended).
  • Bind variables are unaffected: :param, @var, WHERE x = :id still tokenize as TOKEN_TYPE_VARIABLE (covered by a new tokenizer test). PostgreSQL casts (1::text, '{}'::json) are unaffected — in :: the second : is preceded by :, not a word character.
  • One intentional re-tokenization: a : glued to a preceding identifier or number (e.g. the :5 in a PostgreSQL array slice arr[1:5]) was VARIABLE and becomes BOUNDARY + WORD. No fixture covers it and rendered output is equivalent; such a colon is never a placeholder in any supported dialect.

Tests

  • New tokenizer cases: bar:loopWORD + BOUNDARY + WORD, and :paramVARIABLE.
  • New format regression fixture (the procedure above), expected outputs regenerated with bin/regenerate-expected-output.
  • phpunit, phpstan analyse and phpcs all pass.

Relates to #118 (formatter must never fail).

🤖 Generated with Claude Code

A colon immediately preceded by a word character is a label separator
(MySQL/Oracle label:LOOP), never a bind-variable prefix. The tokenizer
lexed bar:LOOP as WORD('bar') + VARIABLE(':LOOP'), swallowing the LOOP
keyword: the indent-opener branch in SqlFormatter::format() never fired
while END LOOP and END still decremented the indent level, driving it
to -1 and crashing str_repeat() with a ValueError.

Guard the ':' alternative of the VARIABLE pattern with a negative
lookbehind (?<!\w); '@' keeps its unconditional behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greg0ire greg0ire added the bug Something isn't working label Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants