Skip to content

fix: keep a dot in front of a number in an unsupported statement - #2895

Closed
simpleqt wants to merge 1 commit into
JSQLParser:masterfrom
simpleqt:fix/capture-rest-dot-before-number
Closed

simpleqt wants to merge 1 commit into
JSQLParser:masterfrom
simpleqt:fix/capture-rest-dot-before-number

Conversation

@simpleqt

@simpleqt simpleqt commented Nov 4, 2026

Copy link
Copy Markdown

captureRest() collapses a dot into the token in front of it whenever the previous token ends with a dot or the token is a dot, without looking at what follows. A dot which starts a number is therefore glued to its neighbour, and the captured token stream no longer reads back the same way once the statement is rendered and parsed again.

CCJSqlParserUtil.parse("REPLACE( . 5").toString();                      // REPLACE (.5
CCJSqlParserUtil.parse("REPLACE( . 5").toString()                        // -> REPLACE ( .5
CCJSqlParserUtil.parse("REPLACE( INTO t VALUES (1, . 5)").toString();    // REPLACE ( INTO t VALUES ( 1 ,.5 )
CCJSqlParserUtil.parse("REPLACE( x . 1 . 2").toString();                 // REPLACE ( x.1.2  -> x.1.2 is three tokens

The lexer reads .5 and 5. as single doubles, so joining a dot in that position changes the token stream that the statement was built from.

Fix

captureRest() now only uses a dot to collapse a qualified name when the dot is a qualifier (src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt). A dot which is followed by a number is kept as its own token, and a number in front of a dot is not extended:

REPLACE( . 5                        -> REPLACE ( . 5
REPLACE( INTO t VALUES (1, . 5)     -> REPLACE ( INTO t VALUES ( 1 , . 5 )
REPLACE( x . 1 . 2                  -> REPLACE ( x . 1 . 2
REPLACE( t . a . b                  -> REPLACE ( t.a.b      (qualified name still collapses)
REPLACE( a . b                      -> REPLACE ( a.b

Tests

Two tests in UnsupportedStatementTest:

  • testCaptureRestKeepsADotInFrontOfANumber checks that the captured tokens read back the same way, and that the rendered statement parses to itself.
  • testCaptureRestStillCollapsesAQualifiedName pins the qualified name case, including the quoted "inter"."inter_user_rec" from testCaptureRestIssue1993.

On the parent commit the new test fails with

org.opentest4j.AssertionFailedError: expected: <REPLACE (.5> but was: <REPLACE ( .5>

and with the fix the suite passes:

Tests run: 12411, Failures: 0, Errors: 0, Skipped: 25
BUILD SUCCESS

mvn verify was run locally on JDK 17 with -Dtoolchain.jdk.version=[17,26). The change is confined to JAVACODE methods, so no grammar production is altered and no choice conflict is introduced.

Ref #386 and #1993, which are about the same token echo.

captureRest() collapsed a dot into the token in front of it whenever the
previous token ended with a dot or the current token was a dot, without
looking at what followed. A dot which starts a number was therefore glued
to its neighbour, and the captured token stream no longer read back the
same way once it was rendered and parsed again:

    REPLACE( . 5          captured as   REPLACE ( .5
    REPLACE( INTO t VALUES (1, . 5)     REPLACE ( INTO t VALUES ( 1 ,.5 )
    REPLACE( x . 1 . 2    captured as   REPLACE ( x.1.2

toString() of such a statement is not a fixpoint, and the token stream it
prints is not what the input contained.

The dot is now only used to collapse a qualified name when it is a
qualifier. A dot which is followed by a number, and a number in front of
a dot, keep the tokens apart. A qualified name still collapses:

    REPLACE( t . a . b    captured as   REPLACE ( t.a.b
    REPLACE( a . b        captured as   REPLACE ( a.b
Copilot AI balanced review requested due to automatic review settings November 4, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@Sinkleberg

Copy link
Copy Markdown

One token-preservation case remains on this head: REPLACE( 5 . a renders as REPLACE ( 5.a, changing the lexer tokens S_LONG(5), '.', identifier(a) into S_DOUBLE(5.), identifier(a). This already happens on the parent too, but the new whitespace-stripped comparison and rendered-string round trip both pass for it; comparing the original and rendered lexer tokens would catch it.

@manticore-projects

Copy link
Copy Markdown
Contributor

I don't see the benefit in adding the complexity. Do you have any practical relevant use-case?

@simpleqt

simpleqt commented Nov 5, 2026

Copy link
Copy Markdown
Author

You're right, and I could not find the use case either. What I checked:

  • The trigger has to be a dot that is a token of its own and sits next to a number in the source, i.e. written x . 5 or 5 . a. Without those spaces the lexer already reads .5 / 5. as a single double, so there is nothing to glue and nothing changes.
  • No .sql file under src/test/resources (321 files) contains x . 5, 5 . a, or even a spaced qualifier a . b. Parsing all of them with withUnsupportedStatements(true) produces exactly one UnsupportedStatement, and its captured tokens re-lex as themselves.
  • The only SQL strings in the repository with that shape are the six I added to UnsupportedStatementTest myself.

The patch also does not establish its own invariant: on this head, REPLACE( 5 . a still renders as REPLACE ( 5.a, which the lexer reads as S_DOUBLE(5.) + a instead of S_LONG(5), ., a. And the same glueing is in captureFunctionBody() and captureCompoundStatement() (lines 25236 and 17874 of the grammar), which this patch does not touch, so the three capture paths would disagree with each other.

Closing it rather than adding complexity you have to carry. If you ever want it, it is a handful of lines in captureRest() plus the reverse operand order -- but as one shared helper for all three capture paths, which is more than this is worth without a report behind it.

@simpleqt simpleqt closed this Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants